This is an automated email from the ASF dual-hosted git repository.
rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git
The following commit(s) were added to refs/heads/master by this push:
new d86c415 Apply standard scala formatting. (#2650)
d86c415 is described below
commit d86c415a1d9a4eaa2061eac3a6ce99f24ba390f9
Author: Markus Thömmes <markusthoemmes@me.com>
AuthorDate: Wed Sep 6 20:53:48 2017 +0200
Apply standard scala formatting. (#2650)
Formats all .scala files according to `scalafmt`'s (opinionated) style.
Adds Travis checks for correctly formatted code.
---
.scalafmt.conf | 6 +
build.gradle | 13 +
.../scala/src/main/scala/whisk/common/Config.scala | 191 +-
.../src/main/scala/whisk/common/Counter.scala | 36 +-
.../src/main/scala/whisk/common/DateUtil.scala | 34 +-
.../src/main/scala/whisk/common/Logging.scala | 332 +-
.../src/main/scala/whisk/common/RingBuffer.scala | 8 +-
.../src/main/scala/whisk/common/Scheduler.scala | 164 +-
.../src/main/scala/whisk/common/SimpleExec.scala | 59 +-
.../src/main/scala/whisk/common/TimingUtil.scala | 10 +-
.../main/scala/whisk/common/TransactionId.scala | 255 +-
.../connector/kafka/KafkaConsumerConnector.scala | 104 +-
.../connector/kafka/KafkaMessagingProvider.scala | 9 +-
.../connector/kafka/KafkaProducerConnector.scala | 84 +-
.../src/main/scala/whisk/core/WhiskConfig.scala | 318 +-
.../main/scala/whisk/core/connector/Message.scala | 110 +-
.../whisk/core/connector/MessageConsumer.scala | 324 +-
.../whisk/core/connector/MessageProducer.scala | 13 +-
.../whisk/core/connector/MessagingProvider.scala | 8 +-
.../scala/whisk/core/database/ArtifactStore.scala | 136 +-
.../core/database/ArtifactStoreProvider.scala | 9 +-
.../whisk/core/database/CloudantRestClient.scala | 12 +-
.../whisk/core/database/CouchDbRestClient.scala | 422 +--
.../whisk/core/database/CouchDbRestStore.scala | 508 ++--
.../whisk/core/database/CouchDbStoreProvider.scala | 29 +-
.../whisk/core/database/DocumentFactory.scala | 333 +-
.../MultipleReadersSingleWriterCache.scala | 726 ++---
.../core/database/RemoteCacheInvalidation.scala | 65 +-
.../scala/whisk/core/entitlement/Privilege.scala | 27 +-
.../whisk/core/entity/ActivationEntityLimit.scala | 2 +-
.../scala/whisk/core/entity/ActivationId.scala | 161 +-
.../scala/whisk/core/entity/ActivationLogs.scala | 27 +-
.../scala/whisk/core/entity/ActivationResult.scala | 321 +-
.../scala/whisk/core/entity/ArgNormalizer.scala | 38 +-
.../main/scala/whisk/core/entity/Attachments.scala | 95 +-
.../src/main/scala/whisk/core/entity/AuthKey.scala | 107 +-
.../main/scala/whisk/core/entity/CacheKey.scala | 36 +-
.../src/main/scala/whisk/core/entity/DocInfo.scala | 144 +-
.../main/scala/whisk/core/entity/EntityPath.scala | 320 +-
.../src/main/scala/whisk/core/entity/Exec.scala | 360 +--
.../scala/whisk/core/entity/ExecManifest.scala | 381 +--
.../core/entity/FullyQualifiedEntityName.scala | 104 +-
.../main/scala/whisk/core/entity/Identity.scala | 162 +-
.../main/scala/whisk/core/entity/InstanceId.scala | 4 +-
.../src/main/scala/whisk/core/entity/Limits.scala | 51 +-
.../main/scala/whisk/core/entity/LogLimit.scala | 63 +-
.../main/scala/whisk/core/entity/MemoryLimit.scala | 64 +-
.../main/scala/whisk/core/entity/Parameter.scala | 307 +-
.../src/main/scala/whisk/core/entity/Secret.scala | 74 +-
.../src/main/scala/whisk/core/entity/SemVer.scala | 103 +-
.../src/main/scala/whisk/core/entity/Size.scala | 163 +-
.../src/main/scala/whisk/core/entity/Subject.scala | 68 +-
.../main/scala/whisk/core/entity/TimeLimit.scala | 71 +-
.../src/main/scala/whisk/core/entity/UUID.scala | 58 +-
.../main/scala/whisk/core/entity/WhiskAction.scala | 439 +--
.../scala/whisk/core/entity/WhiskActivation.scala | 103 +-
.../main/scala/whisk/core/entity/WhiskAuth.scala | 42 +-
.../main/scala/whisk/core/entity/WhiskEntity.scala | 224 +-
.../scala/whisk/core/entity/WhiskPackage.scala | 351 +--
.../main/scala/whisk/core/entity/WhiskRule.scala | 269 +-
.../main/scala/whisk/core/entity/WhiskStore.scala | 491 +--
.../scala/whisk/core/entity/WhiskTrigger.scala | 102 +-
.../main/scala/whisk/http/BasicHttpService.scala | 174 +-
.../main/scala/whisk/http/BasicRasService.scala | 20 +-
.../src/main/scala/whisk/http/ErrorResponse.scala | 308 +-
.../scala/src/main/scala/whisk/spi/SpiLoader.scala | 31 +-
.../whisk/utils/ExecutionContextFactory.scala | 75 +-
.../src/main/scala/whisk/utils/JsHelpers.scala | 29 +-
.../scala/src/main/scala/whisk/utils/Retry.scala | 43 +-
.../main/scala/whisk/core/controller/Actions.scala | 1123 +++----
.../scala/whisk/core/controller/Activations.scala | 342 ++-
.../scala/whisk/core/controller/ApiUtils.scala | 559 ++--
.../scala/whisk/core/controller/Authenticate.scala | 72 +-
.../whisk/core/controller/AuthenticatedRoute.scala | 29 +-
.../controller/AuthorizedRouteDispatcher.scala | 198 +-
.../main/scala/whisk/core/controller/Backend.scala | 17 +-
.../scala/whisk/core/controller/Controller.scala | 246 +-
.../scala/whisk/core/controller/Entities.scala | 187 +-
.../scala/whisk/core/controller/Namespaces.scala | 138 +-
.../scala/whisk/core/controller/Packages.scala | 524 ++--
.../scala/whisk/core/controller/RestAPIs.scala | 414 ++-
.../main/scala/whisk/core/controller/Rules.scala | 692 +++--
.../scala/whisk/core/controller/Triggers.scala | 541 ++--
.../scala/whisk/core/controller/WebActions.scala | 1171 +++----
.../controller/actions/PostActionActivation.scala | 61 +-
.../core/controller/actions/PrimitiveActions.scala | 509 ++--
.../core/controller/actions/SequenceActions.scala | 807 ++---
.../whisk/core/entitlement/ActionCollection.scala | 50 +-
.../core/entitlement/ActivationThrottler.scala | 42 +-
.../scala/whisk/core/entitlement/Collection.scala | 179 +-
.../scala/whisk/core/entitlement/Entitlement.scala | 471 +--
.../whisk/core/entitlement/LocalEntitlement.scala | 64 +-
.../whisk/core/entitlement/PackageCollection.scala | 184 +-
.../whisk/core/entitlement/RateThrottler.scala | 89 +-
.../core/loadBalancer/InvokerSupervision.scala | 477 +--
.../whisk/core/loadBalancer/LoadBalancerData.scala | 155 +-
.../core/loadBalancer/LoadBalancerService.scala | 555 ++--
.../scala/whisk/core/containerpool/Container.scala | 39 +-
.../whisk/core/containerpool/ContainerPool.scala | 313 +-
.../whisk/core/containerpool/ContainerProxy.scala | 661 ++--
.../docker/DockerActionLogDriver.scala | 147 +-
.../core/containerpool/docker/DockerClient.scala | 246 +-
.../docker/DockerClientWithFileAccess.scala | 283 +-
.../containerpool/docker/DockerContainer.scala | 396 +--
.../core/containerpool/docker/HttpUtils.scala | 176 +-
.../core/containerpool/docker/ProcessRunner.scala | 47 +-
.../core/containerpool/docker/RuncClient.scala | 56 +-
.../main/scala/whisk/core/invoker/Invoker.scala | 108 +-
.../scala/whisk/core/invoker/InvokerReactive.scala | 350 ++-
.../scala/whisk/core/invoker/InvokerServer.scala | 6 +-
settings.gradle | 5 +
.../scala/actionContainers/ActionContainer.scala | 211 +-
.../ActionProxyContainerTests.scala | 424 +--
.../DockerExampleContainerTests.scala | 190 +-
.../JavaActionContainerTests.scala | 304 +-
.../NodeJsActionContainerTests.scala | 553 ++--
.../Php71ActionContainerTests.scala | 494 ++-
.../Python2ActionContainerTests.scala | 6 +-
.../PythonActionContainerTests.scala | 507 ++--
.../scala/actionContainers/ResourceHelpers.scala | 255 +-
.../Swift311ActionContainerTests.scala | 6 +-
.../Swift3ActionContainerTests.scala | 297 +-
.../apigw/healthtests/ApiGwEndToEndTests.scala | 227 +-
tests/src/test/scala/common/JsHelpers.scala | 14 +-
tests/src/test/scala/common/LoggedFunction.scala | 49 +-
tests/src/test/scala/common/StreamLogging.scala | 8 +-
tests/src/test/scala/common/TestHelpers.scala | 20 +-
tests/src/test/scala/common/Wsk.scala | 1888 ++++++------
tests/src/test/scala/common/WskActorSystem.scala | 22 +-
tests/src/test/scala/common/WskTestHelpers.scala | 416 +--
.../src/test/scala/ha/CacheInvalidationTests.scala | 213 +-
tests/src/test/scala/limits/ThrottleTests.scala | 584 ++--
tests/src/test/scala/services/HeadersTests.scala | 443 ++-
.../test/scala/services/KafkaConnectorTests.scala | 122 +-
.../test/scala/system/basic/WskActionTests.scala | 539 ++--
.../scala/system/basic/WskBasicJavaTests.scala | 126 +-
.../scala/system/basic/WskBasicNodeTests.scala | 206 +-
.../scala/system/basic/WskBasicPythonTests.scala | 193 +-
.../scala/system/basic/WskBasicSwift311Tests.scala | 5 +-
.../scala/system/basic/WskBasicSwift3Tests.scala | 53 +-
.../test/scala/system/basic/WskBasicTests.scala | 1628 +++++-----
.../test/scala/system/basic/WskConsoleTests.scala | 84 +-
.../test/scala/system/basic/WskPackageTests.scala | 169 +-
.../src/test/scala/system/basic/WskRuleTests.scala | 706 ++---
.../src/test/scala/system/basic/WskSdkTests.scala | 152 +-
.../test/scala/system/basic/WskSequenceTests.scala | 936 +++---
.../scala/system/basic/WskUnicodeJavaTests.scala | 9 +-
.../scala/system/basic/WskUnicodeNodeTests.scala | 9 +-
.../system/basic/WskUnicodePython2Tests.scala | 9 +-
.../system/basic/WskUnicodePython3Tests.scala | 9 +-
.../system/basic/WskUnicodeSwift311Tests.scala | 9 +-
.../scala/system/basic/WskUnicodeSwift3Tests.scala | 9 +-
.../test/scala/system/basic/WskUnicodeTests.scala | 49 +-
.../test/scala/system/rest/ActionSchemaTests.scala | 143 +-
.../test/scala/system/rest/GoCLINginxTests.scala | 84 +-
tests/src/test/scala/system/rest/JsonSchema.scala | 24 +-
.../test/scala/system/rest/JsonSchemaTests.scala | 38 +-
tests/src/test/scala/system/rest/RestUtil.scala | 80 +-
.../src/test/scala/system/rest/SwaggerTests.scala | 43 +-
.../src/test/scala/whisk/common/ConfigTests.scala | 80 +-
.../test/scala/whisk/common/SchedulerTests.scala | 297 +-
.../test/scala/whisk/core/WhiskConfigTests.scala | 121 +-
.../scala/whisk/core/admin/WskAdminTests.scala | 275 +-
.../actions/test/ApiGwRoutemgmtActionTests.scala | 781 +++--
.../scala/whisk/core/cli/test/ApiGwTests.scala | 1839 +++++------
.../whisk/core/cli/test/JsonArgsForTests.scala | 275 +-
.../core/cli/test/SequenceMigrationTests.scala | 158 +-
.../scala/whisk/core/cli/test/Swift311Tests.scala | 5 +-
.../scala/whisk/core/cli/test/Swift3Tests.scala | 216 +-
.../core/cli/test/WskActionSequenceTests.scala | 81 +-
.../whisk/core/cli/test/WskBasicUsageTests.scala | 3191 ++++++++++----------
.../scala/whisk/core/cli/test/WskConfigTests.scala | 448 +--
.../whisk/core/cli/test/WskEntitlementTests.scala | 557 ++--
.../whisk/core/cli/test/WskWebActionsTests.scala | 530 ++--
.../core/connector/test/MessageFeedTests.scala | 244 +-
.../whisk/core/connector/test/TestConnector.scala | 117 +-
.../connector/tests/CompletionMessageTests.scala | 69 +-
.../docker/test/ActionLogDriverTests.scala | 175 +-
.../docker/test/ContainerConnectionTests.scala | 156 +-
.../docker/test/DockerClientTests.scala | 200 +-
.../test/DockerClientWithFileAccessTests.scala | 232 +-
.../docker/test/DockerContainerTests.scala | 1103 +++----
.../docker/test/ProcessRunnerTests.scala | 32 +-
.../docker/test/RuncClientTests.scala | 80 +-
.../containerpool/test/ContainerPoolTests.scala | 686 ++---
.../containerpool/test/ContainerProxyTests.scala | 940 +++---
.../actions/test/ActivationFinisherTests.scala | 229 +-
.../actions/test/SequenceAccountingTests.scala | 200 +-
.../core/controller/test/ActionsApiTests.scala | 1544 +++++-----
.../core/controller/test/ActivationsApiTests.scala | 640 ++--
.../core/controller/test/AuthenticateTests.scala | 164 +-
.../core/controller/test/AuthenticateV2Tests.scala | 34 +-
.../controller/test/ControllerTestCommon.scala | 260 +-
.../controller/test/EntitlementProviderTests.scala | 1108 +++----
.../core/controller/test/NamespacesApiTests.scala | 96 +-
.../controller/test/PackageActionsApiTests.scala | 1153 +++----
.../core/controller/test/PackagesApiTests.scala | 1336 ++++----
.../core/controller/test/RateThrottleTests.scala | 49 +-
.../controller/test/RespondWithHeadersTests.scala | 68 +-
.../whisk/core/controller/test/RulesApiTests.scala | 1548 +++++-----
.../core/controller/test/SequenceApiTests.scala | 729 ++---
.../core/controller/test/SwaggerRoutesTests.scala | 28 +-
.../core/controller/test/TriggersApiTests.scala | 604 ++--
.../core/controller/test/WebActionsApiTests.scala | 2268 +++++++-------
.../core/controller/test/WhiskAuthHelpers.scala | 12 +-
.../SequenceActionApiMigrationTests.scala | 247 +-
.../core/database/test/CacheConcurrencyTests.scala | 178 +-
.../database/test/CleanUpActivationsTest.scala | 162 +-
.../database/test/CouchDbRestClientTests.scala | 341 ++-
.../database/test/DatabaseScriptTestUtils.scala | 140 +-
.../scala/whisk/core/database/test/DbUtils.scala | 324 +-
.../database/test/ExtendedCouchDbRestClient.scala | 70 +-
.../MultipleReadersSingleWriterCacheTests.scala | 51 +-
.../whisk/core/database/test/RemoveLogsTests.scala | 137 +-
.../whisk/core/database/test/ReplicatorTests.scala | 760 ++---
.../core/entity/test/ActivationResponseTests.scala | 308 +-
.../whisk/core/entity/test/DatastoreTests.scala | 518 ++--
.../scala/whisk/core/entity/test/ExecHelpers.scala | 70 +-
.../whisk/core/entity/test/ExecManifestTests.scala | 293 +-
.../whisk/core/entity/test/MigrationEntities.scala | 63 +-
.../scala/whisk/core/entity/test/SchemaTests.scala | 1274 ++++----
.../scala/whisk/core/entity/test/SizeTests.scala | 230 +-
.../scala/whisk/core/entity/test/ViewTests.scala | 671 ++--
.../whisk/core/entity/test/WhiskEntityTests.scala | 195 +-
.../whisk/core/limits/ActionLimitsTests.scala | 407 ++-
.../whisk/core/limits/MaxActionDurationTests.scala | 62 +-
.../test/InvokerSupervisionTests.scala | 343 ++-
.../loadBalancer/test/LoadBalancerDataTests.scala | 226 +-
.../test/LoadBalancerServiceObjectTests.scala | 259 +-
tests/src/test/scala/whisk/spi/SpiTests.scala | 28 +-
.../src/test/scala/whisk/test/http/RESTProxy.scala | 175 +-
.../utils/test/ExecutionContextFactoryTests.scala | 16 +-
tools/travis/build.sh | 4 +
233 files changed, 34930 insertions(+), 33204 deletions(-)
diff --git a/.scalafmt.conf b/.scalafmt.conf
new file mode 100644
index 0000000..0dc41ec
--- /dev/null
+++ b/.scalafmt.conf
@@ -0,0 +1,6 @@
+style = intellij
+danglingParentheses = false
+maxColumn = 120
+docstrings = JavaDoc
+rewrite.rules = [SortImports]
+project.git = true
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..300a1ce
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,13 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath "cz.alenkacz:gradle-scalafmt:${gradle.scalafmt.version}"
+ }
+}
+
+subprojects {
+ apply plugin: 'scalafmt'
+ scalafmt.configFilePath = gradle.scalafmt.config
+}
diff --git a/common/scala/src/main/scala/whisk/common/Config.scala b/common/scala/src/main/scala/whisk/common/Config.scala
index ec7b201..9078822 100644
--- a/common/scala/src/main/scala/whisk/common/Config.scala
+++ b/common/scala/src/main/scala/whisk/common/Config.scala
@@ -42,113 +42,116 @@ import scala.util.Try
* @param optionalProperties a Set of optional properties which may or not be defined.
* @param env an optional environment to read from (defaults to sys.env).
*/
-class Config(
- requiredProperties: Map[String, String],
- optionalProperties: Set[String] = Set())(
- env: Map[String, String] = sys.env)(
- implicit logging: Logging) {
-
- private val settings = getProperties().toMap.filter {
- case (k, v) => requiredProperties.contains(k) ||
- (optionalProperties.contains(k) && v != null)
- }
+class Config(requiredProperties: Map[String, String], optionalProperties: Set[String] = Set())(
+ env: Map[String, String] = sys.env)(implicit logging: Logging) {
- lazy val isValid: Boolean = Config.validateProperties(requiredProperties, settings)
-
- /**
- * Gets value for key if it exists else the empty string.
- * The value of the override key will instead be returned if its value is present in the map.
- *
- * @param key to lookup
- * @param overrideKey the property whose value will be returned if the map contains the override key.
- * @return value for the key or the empty string if the key does not have a value/does not exist
- */
- def apply(key: String, overrideKey: String = ""): String = {
- Try(settings(overrideKey)).orElse(Try(settings(key))).getOrElse("")
- }
+ private val settings = getProperties().toMap.filter {
+ case (k, v) =>
+ requiredProperties.contains(k) ||
+ (optionalProperties.contains(k) && v != null)
+ }
- /**
- * Returns the value of a given key.
- *
- * @param key the property that has to be returned.
- */
- def getProperty(key: String): String = {
- this(key)
- }
+ lazy val isValid: Boolean = Config.validateProperties(requiredProperties, settings)
- /**
- * Returns the value of a given key parsed as a double.
- * If parsing fails, return the default value.
- *
- * @param key the property that has to be returned.
- */
- def getAsDouble(key: String, defaultValue: Double): Double = {
- Try { getProperty(key).toDouble } getOrElse { defaultValue }
- }
+ /**
+ * Gets value for key if it exists else the empty string.
+ * The value of the override key will instead be returned if its value is present in the map.
+ *
+ * @param key to lookup
+ * @param overrideKey the property whose value will be returned if the map contains the override key.
+ * @return value for the key or the empty string if the key does not have a value/does not exist
+ */
+ def apply(key: String, overrideKey: String = ""): String = {
+ Try(settings(overrideKey)).orElse(Try(settings(key))).getOrElse("")
+ }
- /**
- * Returns the value of a given key parsed as an integer.
- * If parsing fails, return the default value.
- *
- * @param key the property that has to be returned.
- */
- def getAsInt(key: String, defaultValue: Int): Int = {
- Try { getProperty(key).toInt } getOrElse { defaultValue }
- }
+ /**
+ * Returns the value of a given key.
+ *
+ * @param key the property that has to be returned.
+ */
+ def getProperty(key: String): String = {
+ this(key)
+ }
+
+ /**
+ * Returns the value of a given key parsed as a double.
+ * If parsing fails, return the default value.
+ *
+ * @param key the property that has to be returned.
+ */
+ def getAsDouble(key: String, defaultValue: Double): Double = {
+ Try { getProperty(key).toDouble } getOrElse { defaultValue }
+ }
- /**
- * Converts the set of property to a string for debugging.
- */
- def mkString: String = settings.mkString("\n")
-
- /**
- * Loads the properties from the environment into a mutable map.
- *
- * @return a pair which is the Map defining the properties, and a boolean indicating whether validation succeeded.
- */
- protected def getProperties(): scala.collection.mutable.Map[String, String] = {
- val required = scala.collection.mutable.Map[String, String]() ++= requiredProperties
- Config.readPropertiesFromEnvironment(required, env)
-
- // for optional value, assign them a default from the required properties list
- // to prevent loss of a default value on a required property that may not otherwise be defined
- val optional = scala.collection.mutable.Map[String, String]() ++= optionalProperties.map { k => k -> required.getOrElse(k, null) }
- Config.readPropertiesFromEnvironment(optional, env)
-
- required ++ optional
+ /**
+ * Returns the value of a given key parsed as an integer.
+ * If parsing fails, return the default value.
+ *
+ * @param key the property that has to be returned.
+ */
+ def getAsInt(key: String, defaultValue: Int): Int = {
+ Try { getProperty(key).toInt } getOrElse { defaultValue }
+ }
+
+ /**
+ * Converts the set of property to a string for debugging.
+ */
+ def mkString: String = settings.mkString("\n")
+
+ /**
+ * Loads the properties from the environment into a mutable map.
+ *
+ * @return a pair which is the Map defining the properties, and a boolean indicating whether validation succeeded.
+ */
+ protected def getProperties(): scala.collection.mutable.Map[String, String] = {
+ val required = scala.collection.mutable.Map[String, String]() ++= requiredProperties
+ Config.readPropertiesFromEnvironment(required, env)
+
+ // for optional value, assign them a default from the required properties list
+ // to prevent loss of a default value on a required property that may not otherwise be defined
+ val optional = scala.collection.mutable.Map[String, String]() ++= optionalProperties.map { k =>
+ k -> required.getOrElse(k, null)
}
+ Config.readPropertiesFromEnvironment(optional, env)
+
+ required ++ optional
+ }
}
/**
* Singleton object which provides global methods to manage configuration.
*/
object Config {
- /**
- * Reads a Map of key-value pairs from the environment -- store them in the
- * mutable properties object.
- */
- def readPropertiesFromEnvironment(properties: scala.collection.mutable.Map[String, String], env: Map[String, String])(implicit logging: Logging) = {
- for (p <- properties.keys) {
- val envp = p.replace('.', '_').toUpperCase
- val envv = env.get(envp)
- if (envv.isDefined) {
- logging.info(this, s"environment set value for $p")
- properties += p -> envv.get.trim
- }
- }
+
+ /**
+ * Reads a Map of key-value pairs from the environment -- store them in the
+ * mutable properties object.
+ */
+ def readPropertiesFromEnvironment(properties: scala.collection.mutable.Map[String, String], env: Map[String, String])(
+ implicit logging: Logging) = {
+ for (p <- properties.keys) {
+ val envp = p.replace('.', '_').toUpperCase
+ val envv = env.get(envp)
+ if (envv.isDefined) {
+ logging.info(this, s"environment set value for $p")
+ properties += p -> envv.get.trim
+ }
}
+ }
- /**
- * Checks that the properties object defines all the required properties.
- *
- * @param required a key-value map where the keys are required properties
- * @param properties a set of properties to check
- */
- def validateProperties(required: Map[String, String], properties: Map[String, String])(implicit logging: Logging): Boolean = {
- required.keys.forall { key =>
- val value = properties(key)
- if (value == null) logging.error(this, s"required property $key still not set")
- value != null
- }
+ /**
+ * Checks that the properties object defines all the required properties.
+ *
+ * @param required a key-value map where the keys are required properties
+ * @param properties a set of properties to check
+ */
+ def validateProperties(required: Map[String, String], properties: Map[String, String])(
+ implicit logging: Logging): Boolean = {
+ required.keys.forall { key =>
+ val value = properties(key)
+ if (value == null) logging.error(this, s"required property $key still not set")
+ value != null
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/common/Counter.scala b/common/scala/src/main/scala/whisk/common/Counter.scala
index c08abff..e7fdfe0 100644
--- a/common/scala/src/main/scala/whisk/common/Counter.scala
+++ b/common/scala/src/main/scala/whisk/common/Counter.scala
@@ -23,25 +23,25 @@ import java.util.concurrent.atomic.AtomicLong
* A simple thread-safe counter.
*/
class Counter {
- private val cnt = new AtomicLong(0L)
- def cur = cnt.get()
+ private val cnt = new AtomicLong(0L)
+ def cur = cnt.get()
- /**
- * Increments and gets the current value.
- */
- def next(): Long = {
- cnt.incrementAndGet()
- }
+ /**
+ * Increments and gets the current value.
+ */
+ def next(): Long = {
+ cnt.incrementAndGet()
+ }
- /**
- * Decrements and gets the current value.
- */
- def prev(): Long = {
- cnt.decrementAndGet()
- }
+ /**
+ * Decrements and gets the current value.
+ */
+ def prev(): Long = {
+ cnt.decrementAndGet()
+ }
- /**
- * Sets the value
- */
- def set(i: Long) = cnt.set(i)
+ /**
+ * Sets the value
+ */
+ def set(i: Long) = cnt.set(i)
}
diff --git a/common/scala/src/main/scala/whisk/common/DateUtil.scala b/common/scala/src/main/scala/whisk/common/DateUtil.scala
index 96c4780..2c3a272 100644
--- a/common/scala/src/main/scala/whisk/common/DateUtil.scala
+++ b/common/scala/src/main/scala/whisk/common/DateUtil.scala
@@ -27,26 +27,26 @@ import java.lang.System
*/
object DateUtil {
- /**
- * Returns the current time as a string in yyyy-MM-dd'T'HH:mm:ss.SSSZ format.
- */
- def getTimeString(): String = {
- val now = new Date(System.currentTimeMillis())
- timeFormat.synchronized {
- timeFormat.format(now)
- }
+ /**
+ * Returns the current time as a string in yyyy-MM-dd'T'HH:mm:ss.SSSZ format.
+ */
+ def getTimeString(): String = {
+ val now = new Date(System.currentTimeMillis())
+ timeFormat.synchronized {
+ timeFormat.format(now)
}
+ }
- /**
- * Takes a string in a format given by getTimeString and returns time in epoch millis.
- */
- def parseToMilli(dateStr: String): Long = {
- val date = timeFormat.synchronized {
- timeFormat.parse(dateStr, new java.text.ParsePosition(0))
- }
- date.getTime()
+ /**
+ * Takes a string in a format given by getTimeString and returns time in epoch millis.
+ */
+ def parseToMilli(dateStr: String): Long = {
+ val date = timeFormat.synchronized {
+ timeFormat.parse(dateStr, new java.text.ParsePosition(0))
}
+ date.getTime()
+ }
- private val timeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
+ private val timeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
}
diff --git a/common/scala/src/main/scala/whisk/common/Logging.scala b/common/scala/src/main/scala/whisk/common/Logging.scala
index 7ab7d4a..92c921a 100644
--- a/common/scala/src/main/scala/whisk/common/Logging.scala
+++ b/common/scala/src/main/scala/whisk/common/Logging.scala
@@ -23,103 +23,104 @@ import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
-import akka.event.Logging.{ DebugLevel, InfoLevel, WarningLevel, ErrorLevel }
+import akka.event.Logging.{DebugLevel, ErrorLevel, InfoLevel, WarningLevel}
import akka.event.Logging.LogLevel
import akka.event.LoggingAdapter
trait Logging {
- /**
- * Prints a message on DEBUG level
- *
- * @param from Reference, where the method was called from.
- * @param message Message to write to the log
- */
- def debug(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
- emit(DebugLevel, id, from, message)
- }
-
- /**
- * Prints a message on INFO level
- *
- * @param from Reference, where the method was called from.
- * @param message Message to write to the log
- */
- def info(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
- emit(InfoLevel, id, from, message)
- }
-
- /**
- * Prints a message on WARN level
- *
- * @param from Reference, where the method was called from.
- * @param message Message to write to the log
- */
- def warn(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
- emit(WarningLevel, id, from, message)
- }
- /**
- * Prints a message on ERROR level
- *
- * @param from Reference, where the method was called from.
- * @param message Message to write to the log
- */
- def error(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
- emit(ErrorLevel, id, from, message)
- }
-
- /**
- * Prints a message to the output.
- *
- * @param loglevel The level to log on
- * @param id <code>TransactionId</code> to include in the log
- * @param from Reference, where the method was called from.
- * @param message Message to write to the log
- */
- def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: String)
+ /**
+ * Prints a message on DEBUG level
+ *
+ * @param from Reference, where the method was called from.
+ * @param message Message to write to the log
+ */
+ def debug(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
+ emit(DebugLevel, id, from, message)
+ }
+
+ /**
+ * Prints a message on INFO level
+ *
+ * @param from Reference, where the method was called from.
+ * @param message Message to write to the log
+ */
+ def info(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
+ emit(InfoLevel, id, from, message)
+ }
+
+ /**
+ * Prints a message on WARN level
+ *
+ * @param from Reference, where the method was called from.
+ * @param message Message to write to the log
+ */
+ def warn(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
+ emit(WarningLevel, id, from, message)
+ }
+
+ /**
+ * Prints a message on ERROR level
+ *
+ * @param from Reference, where the method was called from.
+ * @param message Message to write to the log
+ */
+ def error(from: AnyRef, message: String)(implicit id: TransactionId = TransactionId.unknown) = {
+ emit(ErrorLevel, id, from, message)
+ }
+
+ /**
+ * Prints a message to the output.
+ *
+ * @param loglevel The level to log on
+ * @param id <code>TransactionId</code> to include in the log
+ * @param from Reference, where the method was called from.
+ * @param message Message to write to the log
+ */
+ def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: String)
}
/**
* Implementaion of Logging, that uses akka logging.
*/
class AkkaLogging(loggingAdapter: LoggingAdapter) extends Logging {
- def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: String) = {
- val name = if (from.isInstanceOf[String]) from else Logging.getCleanSimpleClassName(from.getClass)
+ def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: String) = {
+ val name = if (from.isInstanceOf[String]) from else Logging.getCleanSimpleClassName(from.getClass)
- val logMessage = Seq(message).collect {
- case msg if msg.nonEmpty =>
- msg.split('\n').map(_.trim).mkString(" ")
- }
-
- val parts = Seq(s"[$id]") ++ Seq(s"[$name]") ++ logMessage
- loggingAdapter.log(loglevel, parts.mkString(" "))
+ val logMessage = Seq(message).collect {
+ case msg if msg.nonEmpty =>
+ msg.split('\n').map(_.trim).mkString(" ")
}
+
+ val parts = Seq(s"[$id]") ++ Seq(s"[$name]") ++ logMessage
+ loggingAdapter.log(loglevel, parts.mkString(" "))
+ }
}
/**
* Implementaion of Logging, that uses the output stream.
*/
class PrintStreamLogging(outputStream: PrintStream = Console.out) extends Logging {
- def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: String) = {
- val now = Instant.now(Clock.systemUTC)
- val time = Emitter.timeFormat.format(now)
- val name = if (from.isInstanceOf[String]) from else Logging.getCleanSimpleClassName(from.getClass)
-
- val level = loglevel match {
- case DebugLevel => "DEBUG"
- case InfoLevel => "INFO"
- case WarningLevel => "WARN"
- case ErrorLevel => "ERROR"
- }
-
- val logMessage = Seq(message).collect {
- case msg if msg.nonEmpty =>
- msg.split('\n').map(_.trim).mkString(" ")
- }
-
- val parts = Seq(s"[$time]", s"[$level]", s"[$id]") ++ Seq(s"[$name]") ++ logMessage
- outputStream.println(parts.mkString(" "))
+ def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: String) = {
+ val now = Instant.now(Clock.systemUTC)
+ val time = Emitter.timeFormat.format(now)
+ val name = if (from.isInstanceOf[String]) from else Logging.getCleanSimpleClassName(from.getClass)
+
+ val level = loglevel match {
+ case DebugLevel => "DEBUG"
+ case InfoLevel => "INFO"
+ case WarningLevel => "WARN"
+ case ErrorLevel => "ERROR"
}
+
+ val logMessage = Seq(message).collect {
+ case msg if msg.nonEmpty =>
+ msg.split('\n').map(_.trim).mkString(" ")
+ }
+
+ val parts = Seq(s"[$time]", s"[$level]", s"[$id]") ++ Seq(s"[$name]") ++ logMessage
+ outputStream.println(parts.mkString(" "))
+ }
}
/**
@@ -132,115 +133,114 @@ class PrintStreamLogging(outputStream: PrintStream = Console.out) extends Loggin
* @param deltaToMarkerStart if this is an end marker, this is the time difference to the start marker
*/
case class LogMarker(token: LogMarkerToken, deltaToTransactionStart: Long, deltaToMarkerStart: Option[Long] = None) {
- override def toString() = {
- val parts = Seq(LogMarker.keyword, token.toString, deltaToTransactionStart) ++ deltaToMarkerStart
- "[" + parts.mkString(":") + "]"
- }
+ override def toString() = {
+ val parts = Seq(LogMarker.keyword, token.toString, deltaToTransactionStart) ++ deltaToMarkerStart
+ "[" + parts.mkString(":") + "]"
+ }
}
object LogMarker {
- val keyword = "marker"
+ val keyword = "marker"
- /** Convenience method for parsing log markers in unit tests. */
- def parse(s: String) = {
- val logmarker = raw"\[${keyword}:([^\s:]+):(\d+)(?::(\d+))?\]".r.unanchored
- val logmarker(token, deltaToTransactionStart, deltaToMarkerStart) = s
- LogMarker(LogMarkerToken.parse(token), deltaToTransactionStart.toLong, Option(deltaToMarkerStart).map(_.toLong))
- }
+ /** Convenience method for parsing log markers in unit tests. */
+ def parse(s: String) = {
+ val logmarker = raw"\[${keyword}:([^\s:]+):(\d+)(?::(\d+))?\]".r.unanchored
+ val logmarker(token, deltaToTransactionStart, deltaToMarkerStart) = s
+ LogMarker(LogMarkerToken.parse(token), deltaToTransactionStart.toLong, Option(deltaToMarkerStart).map(_.toLong))
+ }
}
private object Logging {
- /**
- * Given a class object, return its simple name less the trailing dollar sign.
- */
- def getCleanSimpleClassName(clz: Class[_]) = {
- val simpleName = clz.getSimpleName
- if (simpleName.endsWith("$")) simpleName.dropRight(1)
- else simpleName
- }
+
+ /**
+ * Given a class object, return its simple name less the trailing dollar sign.
+ */
+ def getCleanSimpleClassName(clz: Class[_]) = {
+ val simpleName = clz.getSimpleName
+ if (simpleName.endsWith("$")) simpleName.dropRight(1)
+ else simpleName
+ }
}
private object Emitter {
- val timeFormat = DateTimeFormatter.
- ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").
- withZone(ZoneId.of("UTC"))
+ val timeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneId.of("UTC"))
}
case class LogMarkerToken(component: String, action: String, state: String) {
- override def toString() = component + "_" + action + "_" + state
+ override def toString() = component + "_" + action + "_" + state
- def asFinish = copy(state = LoggingMarkers.finish)
- def asError = copy(state = LoggingMarkers.error)
+ def asFinish = copy(state = LoggingMarkers.finish)
+ def asError = copy(state = LoggingMarkers.error)
}
object LogMarkerToken {
- def parse(s: String) = {
- // Per convention the components are guaranteed to not contain '_'
- // thus it's safe to split at '_' to get the components
- val Array(component, action, state) = s.split("_")
- LogMarkerToken(component, action, state)
- }
+ def parse(s: String) = {
+ // Per convention the components are guaranteed to not contain '_'
+ // thus it's safe to split at '_' to get the components
+ val Array(component, action, state) = s.split("_")
+ LogMarkerToken(component, action, state)
+ }
}
object LoggingMarkers {
- val start = "start"
- val finish = "finish"
- val error = "error"
- val count = "count"
-
- private val controller = "controller"
- private val invoker = "invoker"
- private val database = "database"
- private val activation = "activation"
- private val kafka = "kafka"
- private val loadbalancer = "loadbalancer"
-
- /*
- * Controller related markers
- */
- def CONTROLLER_STARTUP(i: Int) = LogMarkerToken(controller, s"startup$i", count)
-
- // Time of the activation in controller until it is delivered to Kafka
- val CONTROLLER_ACTIVATION = LogMarkerToken(controller, activation, start)
- val CONTROLLER_ACTIVATION_BLOCKING = LogMarkerToken(controller, "blockingActivation", start)
-
- // Time that is needed load balance the activation
- val CONTROLLER_LOADBALANCER = LogMarkerToken(controller, loadbalancer, start)
-
- // Time that is needed to produce message in kafka
- val CONTROLLER_KAFKA = LogMarkerToken(controller, kafka, start)
-
- /*
- * Invoker related markers
- */
- def INVOKER_STARTUP(i: Int) = LogMarkerToken(invoker, s"startup$i", count)
-
- // Check invoker healthy state from loadbalancer
- val LOADBALANCER_INVOKER_OFFLINE = LogMarkerToken(loadbalancer, "invokerOffline", count)
- val LOADBALANCER_INVOKER_UNHEALTHY = LogMarkerToken(loadbalancer, "invokerUnhealthy", count)
-
- // Time that is needed to execute the action
- val INVOKER_ACTIVATION_RUN = LogMarkerToken(invoker, "activationRun", start)
-
- // Time that is needed to init the action
- val INVOKER_ACTIVATION_INIT = LogMarkerToken(invoker, "activationInit", start)
-
- // Time in invoker
- val INVOKER_ACTIVATION = LogMarkerToken(invoker, activation, start)
- def INVOKER_DOCKER_CMD(cmd: String) = LogMarkerToken(invoker, s"docker.$cmd", start)
- def INVOKER_RUNC_CMD(cmd: String) = LogMarkerToken(invoker, s"runc.$cmd", start)
-
- /*
- * General markers
- */
- val DATABASE_CACHE_HIT = LogMarkerToken(database, "cacheHit", count)
- val DATABASE_CACHE_MISS = LogMarkerToken(database, "cacheMiss", count)
- val DATABASE_SAVE = LogMarkerToken(database, "saveDocument", start)
- val DATABASE_DELETE = LogMarkerToken(database, "deleteDocument", start)
- val DATABASE_GET = LogMarkerToken(database, "getDocument", start)
- val DATABASE_QUERY = LogMarkerToken(database, "queryView", start)
- val DATABASE_ATT_GET = LogMarkerToken(database, "getDocumentAttachment", start)
- val DATABASE_ATT_SAVE = LogMarkerToken(database, "saveDocumentAttachment", start)
+ val start = "start"
+ val finish = "finish"
+ val error = "error"
+ val count = "count"
+
+ private val controller = "controller"
+ private val invoker = "invoker"
+ private val database = "database"
+ private val activation = "activation"
+ private val kafka = "kafka"
+ private val loadbalancer = "loadbalancer"
+
+ /*
+ * Controller related markers
+ */
+ def CONTROLLER_STARTUP(i: Int) = LogMarkerToken(controller, s"startup$i", count)
+
+ // Time of the activation in controller until it is delivered to Kafka
+ val CONTROLLER_ACTIVATION = LogMarkerToken(controller, activation, start)
+ val CONTROLLER_ACTIVATION_BLOCKING = LogMarkerToken(controller, "blockingActivation", start)
+
+ // Time that is needed load balance the activation
+ val CONTROLLER_LOADBALANCER = LogMarkerToken(controller, loadbalancer, start)
+
+ // Time that is needed to produce message in kafka
+ val CONTROLLER_KAFKA = LogMarkerToken(controller, kafka, start)
+
+ /*
+ * Invoker related markers
+ */
+ def INVOKER_STARTUP(i: Int) = LogMarkerToken(invoker, s"startup$i", count)
+
+ // Check invoker healthy state from loadbalancer
+ val LOADBALANCER_INVOKER_OFFLINE = LogMarkerToken(loadbalancer, "invokerOffline", count)
+ val LOADBALANCER_INVOKER_UNHEALTHY = LogMarkerToken(loadbalancer, "invokerUnhealthy", count)
+
+ // Time that is needed to execute the action
+ val INVOKER_ACTIVATION_RUN = LogMarkerToken(invoker, "activationRun", start)
+
+ // Time that is needed to init the action
+ val INVOKER_ACTIVATION_INIT = LogMarkerToken(invoker, "activationInit", start)
+
+ // Time in invoker
+ val INVOKER_ACTIVATION = LogMarkerToken(invoker, activation, start)
+ def INVOKER_DOCKER_CMD(cmd: String) = LogMarkerToken(invoker, s"docker.$cmd", start)
+ def INVOKER_RUNC_CMD(cmd: String) = LogMarkerToken(invoker, s"runc.$cmd", start)
+
+ /*
+ * General markers
+ */
+ val DATABASE_CACHE_HIT = LogMarkerToken(database, "cacheHit", count)
+ val DATABASE_CACHE_MISS = LogMarkerToken(database, "cacheMiss", count)
+ val DATABASE_SAVE = LogMarkerToken(database, "saveDocument", start)
+ val DATABASE_DELETE = LogMarkerToken(database, "deleteDocument", start)
+ val DATABASE_GET = LogMarkerToken(database, "getDocument", start)
+ val DATABASE_QUERY = LogMarkerToken(database, "queryView", start)
+ val DATABASE_ATT_GET = LogMarkerToken(database, "getDocumentAttachment", start)
+ val DATABASE_ATT_SAVE = LogMarkerToken(database, "saveDocumentAttachment", start)
}
diff --git a/common/scala/src/main/scala/whisk/common/RingBuffer.scala b/common/scala/src/main/scala/whisk/common/RingBuffer.scala
index 45765df..4f5a6c7 100644
--- a/common/scala/src/main/scala/whisk/common/RingBuffer.scala
+++ b/common/scala/src/main/scala/whisk/common/RingBuffer.scala
@@ -20,13 +20,13 @@ package whisk.common
import org.apache.commons.collections.buffer.CircularFifoBuffer
object RingBuffer {
- def apply[T](size: Int) = new RingBuffer[T](size)
+ def apply[T](size: Int) = new RingBuffer[T](size)
}
class RingBuffer[T](size: Int) {
- private val inner = new CircularFifoBuffer(size)
+ private val inner = new CircularFifoBuffer(size)
- def add(el: T) = inner.add(el)
+ def add(el: T) = inner.add(el)
- def toList() = inner.toArray().asInstanceOf[Array[T]].toList
+ def toList() = inner.toArray().asInstanceOf[Array[T]].toList
}
diff --git a/common/scala/src/main/scala/whisk/common/Scheduler.scala b/common/scala/src/main/scala/whisk/common/Scheduler.scala
index e9be00a..c4bceed 100644
--- a/common/scala/src/main/scala/whisk/common/Scheduler.scala
+++ b/common/scala/src/main/scala/whisk/common/Scheduler.scala
@@ -33,99 +33,93 @@ import akka.actor.Props
* even for asynchronous tasks.
*/
object Scheduler {
- case object WorkOnceNow
- private case object ScheduledWork
+ case object WorkOnceNow
+ private case object ScheduledWork
- /**
- * Sets up an Actor to send itself a message to mimic schedulers behavior in a more controllable way.
- *
- * @param interval the time to wait between two runs
- * @param alwaysWait always wait for the given amount of time or calculate elapsed time to wait
- * @param closure the closure to be executed
- */
- private class Worker(
- initialDelay: FiniteDuration,
- interval: FiniteDuration,
- alwaysWait: Boolean,
- name: String,
- closure: () => Future[Any])(
- implicit logging: Logging,
- transid: TransactionId) extends Actor {
- implicit val ec = context.dispatcher
+ /**
+ * Sets up an Actor to send itself a message to mimic schedulers behavior in a more controllable way.
+ *
+ * @param interval the time to wait between two runs
+ * @param alwaysWait always wait for the given amount of time or calculate elapsed time to wait
+ * @param closure the closure to be executed
+ */
+ private class Worker(initialDelay: FiniteDuration,
+ interval: FiniteDuration,
+ alwaysWait: Boolean,
+ name: String,
+ closure: () => Future[Any])(implicit logging: Logging, transid: TransactionId)
+ extends Actor {
+ implicit val ec = context.dispatcher
- var lastSchedule: Option[Cancellable] = None
+ var lastSchedule: Option[Cancellable] = None
- override def preStart() = {
- if (initialDelay != Duration.Zero) {
- lastSchedule = Some(context.system.scheduler.scheduleOnce(initialDelay, self, ScheduledWork))
- } else {
- self ! ScheduledWork
- }
- }
- override def postStop() = {
- logging.info(this, s"$name shutdown")
- lastSchedule.foreach(_.cancel())
- }
+ override def preStart() = {
+ if (initialDelay != Duration.Zero) {
+ lastSchedule = Some(context.system.scheduler.scheduleOnce(initialDelay, self, ScheduledWork))
+ } else {
+ self ! ScheduledWork
+ }
+ }
+ override def postStop() = {
+ logging.info(this, s"$name shutdown")
+ lastSchedule.foreach(_.cancel())
+ }
- def receive = {
- case WorkOnceNow => Try(closure())
+ def receive = {
+ case WorkOnceNow => Try(closure())
- case ScheduledWork =>
- val deadline = interval.fromNow
- Try(closure()) match {
- case Success(result) =>
- result onComplete { _ =>
- val timeToWait = if (alwaysWait) interval else deadline.timeLeft.max(Duration.Zero)
- // context might be null here if a PoisonPill is sent while doing computations
- lastSchedule = Option(context).map(_.system.scheduler.scheduleOnce(timeToWait, self, ScheduledWork))
- }
+ case ScheduledWork =>
+ val deadline = interval.fromNow
+ Try(closure()) match {
+ case Success(result) =>
+ result onComplete { _ =>
+ val timeToWait = if (alwaysWait) interval else deadline.timeLeft.max(Duration.Zero)
+ // context might be null here if a PoisonPill is sent while doing computations
+ lastSchedule = Option(context).map(_.system.scheduler.scheduleOnce(timeToWait, self, ScheduledWork))
+ }
- case Failure(e) =>
- logging.error(name, s"halted because ${e.getMessage}")
- }
+ case Failure(e) =>
+ logging.error(name, s"halted because ${e.getMessage}")
}
}
+ }
- /**
- * Schedules a closure to run continuously scheduled, with at least the given interval in between runs.
- * This waits until the Future of the closure has finished, ignores its result and waits for at most the
- * time specified. If the closure took as long or longer than the time specified, the next iteration
- * is immediately fired.
- *
- * @param interval the time to wait at most between two runs of the closure
- * @param initialDelay optionally delay the first scheduled iteration by given duration
- * @param f the function to run
- */
- def scheduleWaitAtMost(
- interval: FiniteDuration,
- initialDelay: FiniteDuration = Duration.Zero,
- name: String = "Scheduler")(
- f: () => Future[Any])(
- implicit system: ActorSystem,
- logging: Logging,
- transid: TransactionId = TransactionId.unknown) = {
- require(interval > Duration.Zero)
- system.actorOf(Props(new Worker(initialDelay, interval, false, name, f)))
- }
+ /**
+ * Schedules a closure to run continuously scheduled, with at least the given interval in between runs.
+ * This waits until the Future of the closure has finished, ignores its result and waits for at most the
+ * time specified. If the closure took as long or longer than the time specified, the next iteration
+ * is immediately fired.
+ *
+ * @param interval the time to wait at most between two runs of the closure
+ * @param initialDelay optionally delay the first scheduled iteration by given duration
+ * @param f the function to run
+ */
+ def scheduleWaitAtMost(interval: FiniteDuration,
+ initialDelay: FiniteDuration = Duration.Zero,
+ name: String = "Scheduler")(f: () => Future[Any])(implicit system: ActorSystem,
+ logging: Logging,
+ transid: TransactionId =
+ TransactionId.unknown) = {
+ require(interval > Duration.Zero)
+ system.actorOf(Props(new Worker(initialDelay, interval, false, name, f)))
+ }
- /**
- * Schedules a closure to run continuously scheduled, with at least the given interval in between runs.
- * This waits until the Future of the closure has finished, ignores its result and then waits for the
- * given interval.
- *
- * @param interval the time to wait between two runs of the closure
- * @param initialDelay optionally delay the first scheduled iteration by given duration
- * @param f the function to run
- */
- def scheduleWaitAtLeast(
- interval: FiniteDuration,
- initialDelay: FiniteDuration = Duration.Zero,
- name: String = "Scheduler")(
- f: () => Future[Any])(
- implicit system: ActorSystem,
- logging: Logging,
- transid: TransactionId = TransactionId.unknown) = {
- require(interval > Duration.Zero)
- system.actorOf(Props(new Worker(initialDelay, interval, true, name, f)))
- }
+ /**
+ * Schedules a closure to run continuously scheduled, with at least the given interval in between runs.
+ * This waits until the Future of the closure has finished, ignores its result and then waits for the
+ * given interval.
+ *
+ * @param interval the time to wait between two runs of the closure
+ * @param initialDelay optionally delay the first scheduled iteration by given duration
+ * @param f the function to run
+ */
+ def scheduleWaitAtLeast(interval: FiniteDuration,
+ initialDelay: FiniteDuration = Duration.Zero,
+ name: String = "Scheduler")(f: () => Future[Any])(implicit system: ActorSystem,
+ logging: Logging,
+ transid: TransactionId =
+ TransactionId.unknown) = {
+ require(interval > Duration.Zero)
+ system.actorOf(Props(new Worker(initialDelay, interval, true, name, f)))
+ }
}
diff --git a/common/scala/src/main/scala/whisk/common/SimpleExec.scala b/common/scala/src/main/scala/whisk/common/SimpleExec.scala
index ce49c82..abe520c 100644
--- a/common/scala/src/main/scala/whisk/common/SimpleExec.scala
+++ b/common/scala/src/main/scala/whisk/common/SimpleExec.scala
@@ -24,35 +24,34 @@ import scala.sys.process.stringSeqToProcess
* Utility to exec processes
*/
object SimpleExec {
- /**
- * Runs a external process.
- *
- * @param cmd an array of String -- their concatenation is the command to exec
- * @return a triple of (stdout, stderr, exitcode) from running the command
- */
- def syncRunCmd(cmd: Seq[String])(implicit transid: TransactionId, logging: Logging): (String, String, Int) = {
- logging.info(this, s"Running command: ${cmd.mkString(" ")}")
- val pb = stringSeqToProcess(cmd)
-
- val outs = new StringBuilder()
- val errs = new StringBuilder()
-
- val exitCode = pb ! ProcessLogger(
- outStr => {
- outs.append(outStr)
- outs.append("\n")
- },
- errStr => {
- errs.append(errStr)
- errs.append("\n")
- })
-
- logging.info(this, s"Done running command: ${cmd.mkString(" ")}")
-
- def noLastNewLine(sb: StringBuilder) = {
- if (sb.isEmpty) "" else sb.substring(0, sb.size - 1)
- }
-
- (noLastNewLine(outs), noLastNewLine(errs), exitCode)
+
+ /**
+ * Runs a external process.
+ *
+ * @param cmd an array of String -- their concatenation is the command to exec
+ * @return a triple of (stdout, stderr, exitcode) from running the command
+ */
+ def syncRunCmd(cmd: Seq[String])(implicit transid: TransactionId, logging: Logging): (String, String, Int) = {
+ logging.info(this, s"Running command: ${cmd.mkString(" ")}")
+ val pb = stringSeqToProcess(cmd)
+
+ val outs = new StringBuilder()
+ val errs = new StringBuilder()
+
+ val exitCode = pb ! ProcessLogger(outStr => {
+ outs.append(outStr)
+ outs.append("\n")
+ }, errStr => {
+ errs.append(errStr)
+ errs.append("\n")
+ })
+
+ logging.info(this, s"Done running command: ${cmd.mkString(" ")}")
+
+ def noLastNewLine(sb: StringBuilder) = {
+ if (sb.isEmpty) "" else sb.substring(0, sb.size - 1)
}
+
+ (noLastNewLine(outs), noLastNewLine(errs), exitCode)
+ }
}
diff --git a/common/scala/src/main/scala/whisk/common/TimingUtil.scala b/common/scala/src/main/scala/whisk/common/TimingUtil.scala
index 7ec4d32..a12465c 100644
--- a/common/scala/src/main/scala/whisk/common/TimingUtil.scala
+++ b/common/scala/src/main/scala/whisk/common/TimingUtil.scala
@@ -26,10 +26,10 @@ import scala.concurrent.duration._
*/
object TimingUtil {
- def time[T](blk: => T): (Duration, T) = {
- val start = System.currentTimeMillis
- val res = blk
- ((System.currentTimeMillis - start).millis, res)
- }
+ def time[T](blk: => T): (Duration, T) = {
+ val start = System.currentTimeMillis
+ val res = blk
+ ((System.currentTimeMillis - start).millis, res)
+ }
}
diff --git a/common/scala/src/main/scala/whisk/common/TransactionId.scala b/common/scala/src/main/scala/whisk/common/TransactionId.scala
index c05034b..08fa6d9 100644
--- a/common/scala/src/main/scala/whisk/common/TransactionId.scala
+++ b/common/scala/src/main/scala/whisk/common/TransactionId.scala
@@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger
import scala.math.BigDecimal.int2bigDecimal
import scala.util.Try
-import akka.event.Logging.{ InfoLevel, WarningLevel }
+import akka.event.Logging.{InfoLevel, WarningLevel}
import akka.event.Logging.LogLevel
import spray.json.JsArray
import spray.json.JsNumber
@@ -39,90 +39,108 @@ import whisk.core.entity.InstanceId
* metadata is stored indirectly in the referenced meta object.
*/
case class TransactionId private (meta: TransactionMetadata) extends AnyVal {
- def id = meta.id
- override def toString = {
- if (meta.id > 0) s"#tid_${meta.id}"
- else if (meta.id < 0) s"#sid_${-meta.id}"
- else "??"
- }
-
- /**
- * Method to count events.
- *
- * @param from Reference, where the method was called from.
- * @param marker A LogMarkerToken. They are defined in <code>LoggingMarkers</code>.
- * @param message An additional message that is written into the log, together with the other information.
- * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.
- */
- def mark(from: AnyRef, marker: LogMarkerToken, message: String = "", logLevel: LogLevel = InfoLevel)(implicit logging: Logging) = {
- logging.emit(logLevel, this, from, createMessageWithMarker(message, LogMarker(marker, deltaToStart)))
- }
-
- /**
- * Method to start taking time of an action in the code. It returns a <code>StartMarker</code> which has to be
- * passed into the <code>finished</code>-method.
- *
- * @param from Reference, where the method was called from.
- * @param marker A LogMarkerToken. They are defined in <code>LoggingMarkers</code>.
- * @param message An additional message that is written into the log, together with the other information.
- * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.
- *
- * @return startMarker that has to be passed to the finished or failed method to calculate the time difference.
- */
- def started(from: AnyRef, marker: LogMarkerToken, message: String = "", logLevel: LogLevel = InfoLevel)(implicit logging: Logging): StartMarker = {
- logging.emit(logLevel, this, from, createMessageWithMarker(message, LogMarker(marker, deltaToStart)))
- StartMarker(Instant.now, marker)
- }
-
- /**
- * Method to stop taking time of an action in the code. The time the method used will be written into a log message.
- *
- * @param from Reference, where the method was called from.
- * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.
- * @param message An additional message that is written into the log, together with the other information.
- * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.
- * @param endTime Manually set the timestamp of the end. By default it is NOW.
- */
- def finished(from: AnyRef, startMarker: StartMarker, message: String = "", logLevel: LogLevel = InfoLevel, endTime: Instant = Instant.now(Clock.systemUTC))(implicit logging: Logging) = {
- val endMarker = LogMarkerToken(startMarker.startMarker.component, startMarker.startMarker.action, LoggingMarkers.finish)
- logging.emit(logLevel, this, from, createMessageWithMarker(message, LogMarker(endMarker, deltaToStart, Some(deltaToMarker(startMarker, endTime)))))
- }
-
- /**
- * Method to stop taking time of an action in the code that failed. The time the method used will be written into a log message.
- *
- * @param from Reference, where the method was called from.
- * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.
- * @param message An additional message that is written into the log, together with the other information.
- * @param logLevel The <code>LogLevel</code> the message should have. Default is <code>WarningLevel</code>.
- */
- def failed(from: AnyRef, startMarker: StartMarker, message: String = "", logLevel: LogLevel = WarningLevel)(implicit logging: Logging) = {
- val endMarker = LogMarkerToken(startMarker.startMarker.component, startMarker.startMarker.action, LoggingMarkers.error)
- logging.emit(logLevel, this, from, createMessageWithMarker(message, LogMarker(endMarker, deltaToStart, Some(deltaToMarker(startMarker)))))
- }
-
- /**
- * Calculates the time between now and the beginning of the transaction.
- */
- def deltaToStart = Duration.between(meta.start, Instant.now(Clock.systemUTC)).toMillis
-
- /**
- * Calculates the time between now and the startMarker that was returned by <code>starting</code>.
- *
- * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.
- * @param endTime Manually set the endtime. By default it is NOW.
- */
- def deltaToMarker(startMarker: StartMarker, endTime: Instant = Instant.now(Clock.systemUTC)) = Duration.between(startMarker.start, endTime).toMillis
-
- /**
- * Formats log message to include marker.
- *
- * @param message: The log message without the marker
- * @param marker: The marker to add to the message
- */
- private def createMessageWithMarker(message: String, marker: LogMarker): String = {
- (Option(message).filter(_.trim.nonEmpty) ++ Some(marker)).mkString(" ")
- }
+ def id = meta.id
+ override def toString = {
+ if (meta.id > 0) s"#tid_${meta.id}"
+ else if (meta.id < 0) s"#sid_${-meta.id}"
+ else "??"
+ }
+
+ /**
+ * Method to count events.
+ *
+ * @param from Reference, where the method was called from.
+ * @param marker A LogMarkerToken. They are defined in <code>LoggingMarkers</code>.
+ * @param message An additional message that is written into the log, together with the other information.
+ * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.
+ */
+ def mark(from: AnyRef, marker: LogMarkerToken, message: String = "", logLevel: LogLevel = InfoLevel)(
+ implicit logging: Logging) = {
+ logging.emit(logLevel, this, from, createMessageWithMarker(message, LogMarker(marker, deltaToStart)))
+ }
+
+ /**
+ * Method to start taking time of an action in the code. It returns a <code>StartMarker</code> which has to be
+ * passed into the <code>finished</code>-method.
+ *
+ * @param from Reference, where the method was called from.
+ * @param marker A LogMarkerToken. They are defined in <code>LoggingMarkers</code>.
+ * @param message An additional message that is written into the log, together with the other information.
+ * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.
+ *
+ * @return startMarker that has to be passed to the finished or failed method to calculate the time difference.
+ */
+ def started(from: AnyRef, marker: LogMarkerToken, message: String = "", logLevel: LogLevel = InfoLevel)(
+ implicit logging: Logging): StartMarker = {
+ logging.emit(logLevel, this, from, createMessageWithMarker(message, LogMarker(marker, deltaToStart)))
+ StartMarker(Instant.now, marker)
+ }
+
+ /**
+ * Method to stop taking time of an action in the code. The time the method used will be written into a log message.
+ *
+ * @param from Reference, where the method was called from.
+ * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.
+ * @param message An additional message that is written into the log, together with the other information.
+ * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.
+ * @param endTime Manually set the timestamp of the end. By default it is NOW.
+ */
+ def finished(from: AnyRef,
+ startMarker: StartMarker,
+ message: String = "",
+ logLevel: LogLevel = InfoLevel,
+ endTime: Instant = Instant.now(Clock.systemUTC))(implicit logging: Logging) = {
+ val endMarker =
+ LogMarkerToken(startMarker.startMarker.component, startMarker.startMarker.action, LoggingMarkers.finish)
+ logging.emit(
+ logLevel,
+ this,
+ from,
+ createMessageWithMarker(message, LogMarker(endMarker, deltaToStart, Some(deltaToMarker(startMarker, endTime)))))
+ }
+
+ /**
+ * Method to stop taking time of an action in the code that failed. The time the method used will be written into a log message.
+ *
+ * @param from Reference, where the method was called from.
+ * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.
+ * @param message An additional message that is written into the log, together with the other information.
+ * @param logLevel The <code>LogLevel</code> the message should have. Default is <code>WarningLevel</code>.
+ */
+ def failed(from: AnyRef, startMarker: StartMarker, message: String = "", logLevel: LogLevel = WarningLevel)(
+ implicit logging: Logging) = {
+ val endMarker =
+ LogMarkerToken(startMarker.startMarker.component, startMarker.startMarker.action, LoggingMarkers.error)
+ logging.emit(
+ logLevel,
+ this,
+ from,
+ createMessageWithMarker(message, LogMarker(endMarker, deltaToStart, Some(deltaToMarker(startMarker)))))
+ }
+
+ /**
+ * Calculates the time between now and the beginning of the transaction.
+ */
+ def deltaToStart = Duration.between(meta.start, Instant.now(Clock.systemUTC)).toMillis
+
+ /**
+ * Calculates the time between now and the startMarker that was returned by <code>starting</code>.
+ *
+ * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.
+ * @param endTime Manually set the endtime. By default it is NOW.
+ */
+ def deltaToMarker(startMarker: StartMarker, endTime: Instant = Instant.now(Clock.systemUTC)) =
+ Duration.between(startMarker.start, endTime).toMillis
+
+ /**
+ * Formats log message to include marker.
+ *
+ * @param message: The log message without the marker
+ * @param marker: The marker to add to the message
+ */
+ private def createMessageWithMarker(message: String, marker: LogMarker): String = {
+ (Option(message).filter(_.trim.nonEmpty) ++ Some(marker)).mkString(" ")
+ }
}
/**
@@ -143,45 +161,46 @@ case class StartMarker(val start: Instant, startMarker: LogMarkerToken)
protected case class TransactionMetadata(val id: Long, val start: Instant)
object TransactionId {
- val unknown = TransactionId(0)
- val testing = TransactionId(-1) // Common id for for unit testing
- val invoker = TransactionId(-100) // Invoker startup/shutdown or GC activity
- val invokerWarmup = TransactionId(-101) // Invoker warmup thread that makes stem-cell containers
- val invokerNanny = TransactionId(-102) // Invoker nanny thread
- val dispatcher = TransactionId(-110) // Kafka message dispatcher
- val loadbalancer = TransactionId(-120) // Loadbalancer thread
- val invokerHealth = TransactionId(-121) // Invoker supervision
- val controller = TransactionId(-130) // Controller startup
-
- def apply(tid: BigDecimal): TransactionId = {
- Try {
- val now = Instant.now(Clock.systemUTC())
- TransactionId(TransactionMetadata(tid.toLong, now))
- } getOrElse unknown
- }
-
- implicit val serdes = new RootJsonFormat[TransactionId] {
- def write(t: TransactionId) = JsArray(JsNumber(t.meta.id), JsNumber(t.meta.start.toEpochMilli))
-
- def read(value: JsValue) = Try {
- value match {
- case JsArray(Vector(JsNumber(id), JsNumber(start))) =>
- TransactionId(TransactionMetadata(id.longValue, Instant.ofEpochMilli(start.longValue)))
- }
- } getOrElse unknown
- }
+ val unknown = TransactionId(0)
+ val testing = TransactionId(-1) // Common id for for unit testing
+ val invoker = TransactionId(-100) // Invoker startup/shutdown or GC activity
+ val invokerWarmup = TransactionId(-101) // Invoker warmup thread that makes stem-cell containers
+ val invokerNanny = TransactionId(-102) // Invoker nanny thread
+ val dispatcher = TransactionId(-110) // Kafka message dispatcher
+ val loadbalancer = TransactionId(-120) // Loadbalancer thread
+ val invokerHealth = TransactionId(-121) // Invoker supervision
+ val controller = TransactionId(-130) // Controller startup
+
+ def apply(tid: BigDecimal): TransactionId = {
+ Try {
+ val now = Instant.now(Clock.systemUTC())
+ TransactionId(TransactionMetadata(tid.toLong, now))
+ } getOrElse unknown
+ }
+
+ implicit val serdes = new RootJsonFormat[TransactionId] {
+ def write(t: TransactionId) = JsArray(JsNumber(t.meta.id), JsNumber(t.meta.start.toEpochMilli))
+
+ def read(value: JsValue) =
+ Try {
+ value match {
+ case JsArray(Vector(JsNumber(id), JsNumber(start))) =>
+ TransactionId(TransactionMetadata(id.longValue, Instant.ofEpochMilli(start.longValue)))
+ }
+ } getOrElse unknown
+ }
}
/**
* A thread-safe transaction counter.
*/
trait TransactionCounter {
- val numberOfInstances: Int
- val instance: InstanceId
+ val numberOfInstances: Int
+ val instance: InstanceId
- private lazy val cnt = new AtomicInteger(numberOfInstances + instance.toInt)
+ private lazy val cnt = new AtomicInteger(numberOfInstances + instance.toInt)
- def transid(): TransactionId = {
- TransactionId(cnt.addAndGet(numberOfInstances))
- }
+ def transid(): TransactionId = {
+ TransactionId(cnt.addAndGet(numberOfInstances))
+ }
}
diff --git a/common/scala/src/main/scala/whisk/connector/kafka/KafkaConsumerConnector.scala b/common/scala/src/main/scala/whisk/connector/kafka/KafkaConsumerConnector.scala
index a138781..a1116d8 100644
--- a/common/scala/src/main/scala/whisk/connector/kafka/KafkaConsumerConnector.scala
+++ b/common/scala/src/main/scala/whisk/connector/kafka/KafkaConsumerConnector.scala
@@ -32,66 +32,66 @@ import org.apache.kafka.common.serialization.ByteArrayDeserializer
import whisk.common.Logging
import whisk.core.connector.MessageConsumer
-class KafkaConsumerConnector(
- kafkahost: String,
- groupid: String,
- topic: String,
- override val maxPeek: Int = Int.MaxValue,
- readeos: Boolean = true,
- sessionTimeout: FiniteDuration = 30.seconds,
- autoCommitInterval: FiniteDuration = 10.seconds,
- maxPollInterval: FiniteDuration = 5.minutes)(
- implicit logging: Logging)
+class KafkaConsumerConnector(kafkahost: String,
+ groupid: String,
+ topic: String,
+ override val maxPeek: Int = Int.MaxValue,
+ readeos: Boolean = true,
+ sessionTimeout: FiniteDuration = 30.seconds,
+ autoCommitInterval: FiniteDuration = 10.seconds,
+ maxPollInterval: FiniteDuration = 5.minutes)(implicit logging: Logging)
extends MessageConsumer {
- /**
- * Long poll for messages. Method returns once message are available but no later than given
- * duration.
- *
- * @param duration the maximum duration for the long poll
- */
- override def peek(duration: Duration = 500.milliseconds) = {
- val records = consumer.poll(duration.toMillis)
- records map { r => (r.topic, r.partition, r.offset, r.value) }
+ /**
+ * Long poll for messages. Method returns once message are available but no later than given
+ * duration.
+ *
+ * @param duration the maximum duration for the long poll
+ */
+ override def peek(duration: Duration = 500.milliseconds) = {
+ val records = consumer.poll(duration.toMillis)
+ records map { r =>
+ (r.topic, r.partition, r.offset, r.value)
}
+ }
- /**
- * Commits offsets from last poll.
- */
- def commit() = consumer.commitSync()
+ /**
+ * Commits offsets from last poll.
+ */
+ def commit() = consumer.commitSync()
- override def close() = {
- logging.info(this, s"closing '$topic' consumer")
- }
+ override def close() = {
+ logging.info(this, s"closing '$topic' consumer")
+ }
- private def getProps: Properties = {
- val props = new Properties
- props.put(ConsumerConfig.GROUP_ID_CONFIG, groupid)
- props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkahost)
- props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeout.toMillis.toString)
- props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, (sessionTimeout.toMillis / 3).toString)
- props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true.toString)
- props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval.toMillis.toString)
- props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPeek.toString)
- props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, if (!readeos) "latest" else "earliest")
- props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollInterval.toMillis.toString)
+ private def getProps: Properties = {
+ val props = new Properties
+ props.put(ConsumerConfig.GROUP_ID_CONFIG, groupid)
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkahost)
+ props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeout.toMillis.toString)
+ props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, (sessionTimeout.toMillis / 3).toString)
+ props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true.toString)
+ props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval.toMillis.toString)
+ props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPeek.toString)
+ props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, if (!readeos) "latest" else "earliest")
+ props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollInterval.toMillis.toString)
- // This value controls the server-side wait time which affects polling latency.
- // A low value improves latency performance but it is important to not set it too low
- // as that will cause excessive busy-waiting.
- props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, "20")
+ // This value controls the server-side wait time which affects polling latency.
+ // A low value improves latency performance but it is important to not set it too low
+ // as that will cause excessive busy-waiting.
+ props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, "20")
- props
- }
+ props
+ }
- /** Creates a new kafka consumer and subscribes to topic list if given. */
- private def getConsumer(props: Properties, topics: Option[List[String]] = None) = {
- val keyDeserializer = new ByteArrayDeserializer
- val valueDeserializer = new ByteArrayDeserializer
- val consumer = new KafkaConsumer(props, keyDeserializer, valueDeserializer)
- topics foreach { consumer.subscribe(_) }
- consumer
- }
+ /** Creates a new kafka consumer and subscribes to topic list if given. */
+ private def getConsumer(props: Properties, topics: Option[List[String]] = None) = {
+ val keyDeserializer = new ByteArrayDeserializer
+ val valueDeserializer = new ByteArrayDeserializer
+ val consumer = new KafkaConsumer(props, keyDeserializer, valueDeserializer)
+ topics foreach { consumer.subscribe(_) }
+ consumer
+ }
- private val consumer = getConsumer(getProps, Some(List(topic)))
+ private val consumer = getConsumer(getProps, Some(List(topic)))
}
diff --git a/common/scala/src/main/scala/whisk/connector/kafka/KafkaMessagingProvider.scala b/common/scala/src/main/scala/whisk/connector/kafka/KafkaMessagingProvider.scala
index af6afc4..ff6daf1 100644
--- a/common/scala/src/main/scala/whisk/connector/kafka/KafkaMessagingProvider.scala
+++ b/common/scala/src/main/scala/whisk/connector/kafka/KafkaMessagingProvider.scala
@@ -29,9 +29,10 @@ import whisk.core.connector.MessagingProvider
* A Kafka based implementation of MessagingProvider
*/
object KafkaMessagingProvider extends MessagingProvider {
- def getConsumer(config: WhiskConfig, groupId: String, topic: String, maxPeek: Int, maxPollInterval: FiniteDuration)(implicit logging: Logging): MessageConsumer =
- new KafkaConsumerConnector(config.kafkaHost, groupId, topic, maxPeek, maxPollInterval = maxPollInterval)
+ def getConsumer(config: WhiskConfig, groupId: String, topic: String, maxPeek: Int, maxPollInterval: FiniteDuration)(
+ implicit logging: Logging): MessageConsumer =
+ new KafkaConsumerConnector(config.kafkaHost, groupId, topic, maxPeek, maxPollInterval = maxPollInterval)
- def getProducer(config: WhiskConfig, ec: ExecutionContext)(implicit logging: Logging): MessageProducer =
- new KafkaProducerConnector(config.kafkaHost, ec)
+ def getProducer(config: WhiskConfig, ec: ExecutionContext)(implicit logging: Logging): MessageProducer =
+ new KafkaProducerConnector(config.kafkaHost, ec)
}
diff --git a/common/scala/src/main/scala/whisk/connector/kafka/KafkaProducerConnector.scala b/common/scala/src/main/scala/whisk/connector/kafka/KafkaProducerConnector.scala
index 4111cb5..3f524a8 100644
--- a/common/scala/src/main/scala/whisk/connector/kafka/KafkaProducerConnector.scala
+++ b/common/scala/src/main/scala/whisk/connector/kafka/KafkaProducerConnector.scala
@@ -38,58 +38,56 @@ import whisk.common.Logging
import whisk.core.connector.Message
import whisk.core.connector.MessageProducer
-class KafkaProducerConnector(
- kafkahost: String,
- implicit val executionContext: ExecutionContext,
- id: String = UUID.randomUUID().toString)(
- implicit logging: Logging)
+class KafkaProducerConnector(kafkahost: String,
+ implicit val executionContext: ExecutionContext,
+ id: String = UUID.randomUUID().toString)(implicit logging: Logging)
extends MessageProducer {
- override def sentCount() = sentCounter.cur
+ override def sentCount() = sentCounter.cur
- /** Sends msg to topic. This is an asynchronous operation. */
- override def send(topic: String, msg: Message): Future[RecordMetadata] = {
- implicit val transid = msg.transid
- val record = new ProducerRecord[String, String](topic, "messages", msg.serialize)
+ /** Sends msg to topic. This is an asynchronous operation. */
+ override def send(topic: String, msg: Message): Future[RecordMetadata] = {
+ implicit val transid = msg.transid
+ val record = new ProducerRecord[String, String](topic, "messages", msg.serialize)
- logging.debug(this, s"sending to topic '$topic' msg '$msg'")
- val produced = Promise[RecordMetadata]()
- producer.send(record, new Callback {
- override def onCompletion(metadata: RecordMetadata, exception: Exception): Unit = {
- if (exception == null) produced.success(metadata)
- else produced.failure(exception)
- }
- })
+ logging.debug(this, s"sending to topic '$topic' msg '$msg'")
+ val produced = Promise[RecordMetadata]()
+ producer.send(record, new Callback {
+ override def onCompletion(metadata: RecordMetadata, exception: Exception): Unit = {
+ if (exception == null) produced.success(metadata)
+ else produced.failure(exception)
+ }
+ })
- produced.future.andThen {
- case Success(status) =>
- logging.debug(this, s"sent message: ${status.topic()}[${status.partition()}][${status.offset()}]")
- sentCounter.next()
- case Failure(t) =>
- logging.error(this, s"sending message on topic '$topic' failed: ${t.getMessage}")
- }
+ produced.future.andThen {
+ case Success(status) =>
+ logging.debug(this, s"sent message: ${status.topic()}[${status.partition()}][${status.offset()}]")
+ sentCounter.next()
+ case Failure(t) =>
+ logging.error(this, s"sending message on topic '$topic' failed: ${t.getMessage}")
}
+ }
- /** Closes producer. */
- override def close() = {
- logging.info(this, "closing producer")
- producer.close()
- }
+ /** Closes producer. */
+ override def close() = {
+ logging.info(this, "closing producer")
+ producer.close()
+ }
- private val sentCounter = new Counter()
+ private val sentCounter = new Counter()
- private def getProps: Properties = {
- val props = new Properties
- props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkahost)
- props.put(ProducerConfig.ACKS_CONFIG, 1.toString)
- props
- }
+ private def getProps: Properties = {
+ val props = new Properties
+ props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkahost)
+ props.put(ProducerConfig.ACKS_CONFIG, 1.toString)
+ props
+ }
- private def getProducer(props: Properties): KafkaProducer[String, String] = {
- val keySerializer = new StringSerializer
- val valueSerializer = new StringSerializer
- new KafkaProducer(props, keySerializer, valueSerializer)
- }
+ private def getProducer(props: Properties): KafkaProducer[String, String] = {
+ val keySerializer = new StringSerializer
+ val valueSerializer = new StringSerializer
+ new KafkaProducer(props, keySerializer, valueSerializer)
+ }
- private val producer = getProducer(getProps)
+ private val producer = getProducer(getProps)
}
diff --git a/common/scala/src/main/scala/whisk/core/WhiskConfig.scala b/common/scala/src/main/scala/whisk/core/WhiskConfig.scala
index 289090a..2429e44 100644
--- a/common/scala/src/main/scala/whisk/core/WhiskConfig.scala
+++ b/common/scala/src/main/scala/whisk/core/WhiskConfig.scala
@@ -34,189 +34,191 @@ import whisk.common.Logging
* @param optionalProperties a set of optional properties (which may not be defined).
* @param whiskPropertiesFile a File object, the whisk.properties file, which if given contains the property values.
*/
-
-class WhiskConfig(
- requiredProperties: Map[String, String],
- optionalProperties: Set[String] = Set(),
- propertiesFile: File = null,
- env: Map[String, String] = sys.env)(implicit val logging: Logging)
+class WhiskConfig(requiredProperties: Map[String, String],
+ optionalProperties: Set[String] = Set(),
+ propertiesFile: File = null,
+ env: Map[String, String] = sys.env)(implicit val logging: Logging)
extends Config(requiredProperties, optionalProperties)(env) {
- /**
- * Loads the properties as specified above.
- *
- * @return a pair which is the Map defining the properties, and a boolean indicating whether validation succeeded.
- */
- override protected def getProperties() = {
- val properties = super.getProperties()
- WhiskConfig.readPropertiesFromFile(properties, Option(propertiesFile) getOrElse (WhiskConfig.whiskPropertiesFile))
- properties
- }
-
- val servicePort = this(WhiskConfig.servicePort)
- val dockerRegistry = this(WhiskConfig.dockerRegistry)
- val dockerEndpoint = this(WhiskConfig.dockerEndpoint)
- val dockerPort = this(WhiskConfig.dockerPort)
-
- val dockerImagePrefix = this(WhiskConfig.dockerImagePrefix)
- val dockerImageTag = this(WhiskConfig.dockerImageTag)
-
- val invokerContainerNetwork = this(WhiskConfig.invokerContainerNetwork)
- val invokerContainerPolicy = if (this(WhiskConfig.invokerContainerPolicy) == "") None else Some(this(WhiskConfig.invokerContainerPolicy))
- val invokerContainerDns = if (this(WhiskConfig.invokerContainerDns) == "") Seq() else this(WhiskConfig.invokerContainerDns).split(" ").toSeq
- val invokerNumCore = this(WhiskConfig.invokerNumCore)
- val invokerCoreShare = this(WhiskConfig.invokerCoreShare)
-
- val wskApiHost = this(WhiskConfig.wskApiProtocol) + "://" + this(WhiskConfig.wskApiHostname) + ":" + this(WhiskConfig.wskApiPort)
- val controllerBlackboxFraction = this.getAsDouble(WhiskConfig.controllerBlackboxFraction, 0.10)
- val loadbalancerInvokerBusyThreshold = this.getAsInt(WhiskConfig.loadbalancerInvokerBusyThreshold, 16)
- val controllerInstances = this(WhiskConfig.controllerInstances)
-
- val edgeHost = this(WhiskConfig.edgeHostName) + ":" + this(WhiskConfig.edgeHostApiPort)
- val kafkaHost = this(WhiskConfig.kafkaHostName) + ":" + this(WhiskConfig.kafkaHostPort)
-
- val edgeHostName = this(WhiskConfig.edgeHostName)
-
- val zookeeperHost = this(WhiskConfig.zookeeperHostName) + ":" + this(WhiskConfig.zookeeperHostPort)
- val invokerHosts = this(WhiskConfig.invokerHostsList)
-
- val dbProvider = this(WhiskConfig.dbProvider)
- val dbUsername = this(WhiskConfig.dbUsername)
- val dbPassword = this(WhiskConfig.dbPassword)
- val dbProtocol = this(WhiskConfig.dbProtocol)
- val dbHost = this(WhiskConfig.dbHost)
- val dbPort = this(WhiskConfig.dbPort)
- val dbWhisk = this(WhiskConfig.dbWhisk)
- val dbAuths = this(WhiskConfig.dbAuths)
- val dbActivations = this(WhiskConfig.dbActivations)
- val dbPrefix = this(WhiskConfig.dbPrefix)
-
- val mainDockerEndpoint = this(WhiskConfig.mainDockerEndpoint)
-
- val runtimesManifest = this(WhiskConfig.runtimesManifest)
-
- val actionInvokePerMinuteLimit = this(WhiskConfig.actionInvokePerMinuteLimit)
- val actionInvokeConcurrentLimit = this(WhiskConfig.actionInvokeConcurrentLimit)
- val triggerFirePerMinuteLimit = this(WhiskConfig.triggerFirePerMinuteLimit)
- val actionInvokeSystemOverloadLimit = this(WhiskConfig.actionInvokeSystemOverloadLimit)
- val actionSequenceLimit = this(WhiskConfig.actionSequenceMaxLimit)
+ /**
+ * Loads the properties as specified above.
+ *
+ * @return a pair which is the Map defining the properties, and a boolean indicating whether validation succeeded.
+ */
+ override protected def getProperties() = {
+ val properties = super.getProperties()
+ WhiskConfig.readPropertiesFromFile(properties, Option(propertiesFile) getOrElse (WhiskConfig.whiskPropertiesFile))
+ properties
+ }
+
+ val servicePort = this(WhiskConfig.servicePort)
+ val dockerRegistry = this(WhiskConfig.dockerRegistry)
+ val dockerEndpoint = this(WhiskConfig.dockerEndpoint)
+ val dockerPort = this(WhiskConfig.dockerPort)
+
+ val dockerImagePrefix = this(WhiskConfig.dockerImagePrefix)
+ val dockerImageTag = this(WhiskConfig.dockerImageTag)
+
+ val invokerContainerNetwork = this(WhiskConfig.invokerContainerNetwork)
+ val invokerContainerPolicy =
+ if (this(WhiskConfig.invokerContainerPolicy) == "") None else Some(this(WhiskConfig.invokerContainerPolicy))
+ val invokerContainerDns =
+ if (this(WhiskConfig.invokerContainerDns) == "") Seq() else this(WhiskConfig.invokerContainerDns).split(" ").toSeq
+ val invokerNumCore = this(WhiskConfig.invokerNumCore)
+ val invokerCoreShare = this(WhiskConfig.invokerCoreShare)
+
+ val wskApiHost = this(WhiskConfig.wskApiProtocol) + "://" + this(WhiskConfig.wskApiHostname) + ":" + this(
+ WhiskConfig.wskApiPort)
+ val controllerBlackboxFraction = this.getAsDouble(WhiskConfig.controllerBlackboxFraction, 0.10)
+ val loadbalancerInvokerBusyThreshold = this.getAsInt(WhiskConfig.loadbalancerInvokerBusyThreshold, 16)
+ val controllerInstances = this(WhiskConfig.controllerInstances)
+
+ val edgeHost = this(WhiskConfig.edgeHostName) + ":" + this(WhiskConfig.edgeHostApiPort)
+ val kafkaHost = this(WhiskConfig.kafkaHostName) + ":" + this(WhiskConfig.kafkaHostPort)
+
+ val edgeHostName = this(WhiskConfig.edgeHostName)
+
+ val zookeeperHost = this(WhiskConfig.zookeeperHostName) + ":" + this(WhiskConfig.zookeeperHostPort)
+ val invokerHosts = this(WhiskConfig.invokerHostsList)
+
+ val dbProvider = this(WhiskConfig.dbProvider)
+ val dbUsername = this(WhiskConfig.dbUsername)
+ val dbPassword = this(WhiskConfig.dbPassword)
+ val dbProtocol = this(WhiskConfig.dbProtocol)
+ val dbHost = this(WhiskConfig.dbHost)
+ val dbPort = this(WhiskConfig.dbPort)
+ val dbWhisk = this(WhiskConfig.dbWhisk)
+ val dbAuths = this(WhiskConfig.dbAuths)
+ val dbActivations = this(WhiskConfig.dbActivations)
+ val dbPrefix = this(WhiskConfig.dbPrefix)
+
+ val mainDockerEndpoint = this(WhiskConfig.mainDockerEndpoint)
+
+ val runtimesManifest = this(WhiskConfig.runtimesManifest)
+
+ val actionInvokePerMinuteLimit = this(WhiskConfig.actionInvokePerMinuteLimit)
+ val actionInvokeConcurrentLimit = this(WhiskConfig.actionInvokeConcurrentLimit)
+ val triggerFirePerMinuteLimit = this(WhiskConfig.triggerFirePerMinuteLimit)
+ val actionInvokeSystemOverloadLimit = this(WhiskConfig.actionInvokeSystemOverloadLimit)
+ val actionSequenceLimit = this(WhiskConfig.actionSequenceMaxLimit)
}
object WhiskConfig {
- private def whiskPropertiesFile: File = {
- def propfile(dir: String, recurse: Boolean = false): File =
- if (dir != null) {
- val base = new File(dir)
- val file = new File(base, "whisk.properties")
- if (file.exists())
- file
- else if (recurse)
- propfile(base.getParent, true)
- else null
- } else null
-
- val dir = sys.props.get("user.dir")
- if (dir.isDefined) {
- propfile(dir.get, true)
- } else {
- null
- }
- }
+ private def whiskPropertiesFile: File = {
+ def propfile(dir: String, recurse: Boolean = false): File =
+ if (dir != null) {
+ val base = new File(dir)
+ val file = new File(base, "whisk.properties")
+ if (file.exists())
+ file
+ else if (recurse)
+ propfile(base.getParent, true)
+ else null
+ } else null
- /**
- * Reads a Map of key-value pairs from the environment (sys.env) -- store them in the
- * mutable properties object.
- */
- def readPropertiesFromFile(properties: scala.collection.mutable.Map[String, String], file: File)(implicit logging: Logging) = {
- if (file != null && file.exists) {
- logging.info(this, s"reading properties from file $file")
- for (line <- Source.fromFile(file).getLines if line.trim != "") {
- val parts = line.split('=')
- if (parts.length >= 1) {
- val p = parts(0).trim
- val v = if (parts.length == 2) parts(1).trim else ""
- if (properties.contains(p)) {
- properties += p -> v
- logging.debug(this, s"properties file set value for $p")
- }
- } else {
- logging.warn(this, s"ignoring properties $line")
- }
- }
+ val dir = sys.props.get("user.dir")
+ if (dir.isDefined) {
+ propfile(dir.get, true)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Reads a Map of key-value pairs from the environment (sys.env) -- store them in the
+ * mutable properties object.
+ */
+ def readPropertiesFromFile(properties: scala.collection.mutable.Map[String, String], file: File)(
+ implicit logging: Logging) = {
+ if (file != null && file.exists) {
+ logging.info(this, s"reading properties from file $file")
+ for (line <- Source.fromFile(file).getLines if line.trim != "") {
+ val parts = line.split('=')
+ if (parts.length >= 1) {
+ val p = parts(0).trim
+ val v = if (parts.length == 2) parts(1).trim else ""
+ if (properties.contains(p)) {
+ properties += p -> v
+ logging.debug(this, s"properties file set value for $p")
+ }
+ } else {
+ logging.warn(this, s"ignoring properties $line")
}
+ }
}
+ }
- def asEnvVar(key: String): String =
- if (key != null)
- key.replace('.', '_').toUpperCase
- else null
+ def asEnvVar(key: String): String =
+ if (key != null)
+ key.replace('.', '_').toUpperCase
+ else null
- val servicePort = "port"
- val dockerRegistry = "docker.registry"
- val dockerPort = "docker.port"
+ val servicePort = "port"
+ val dockerRegistry = "docker.registry"
+ val dockerPort = "docker.port"
- val dockerEndpoint = "main.docker.endpoint"
+ val dockerEndpoint = "main.docker.endpoint"
- val dbProvider = "db.provider"
- val dbProtocol = "db.protocol"
- val dbHost = "db.host"
- val dbPort = "db.port"
- val dbUsername = "db.username"
- val dbPassword = "db.password"
- val dbWhisk = "db.whisk.actions"
- val dbAuths = "db.whisk.auths"
- val dbPrefix = "db.prefix"
- val dbActivations = "db.whisk.activations"
+ val dbProvider = "db.provider"
+ val dbProtocol = "db.protocol"
+ val dbHost = "db.host"
+ val dbPort = "db.port"
+ val dbUsername = "db.username"
+ val dbPassword = "db.password"
+ val dbWhisk = "db.whisk.actions"
+ val dbAuths = "db.whisk.auths"
+ val dbPrefix = "db.prefix"
+ val dbActivations = "db.whisk.activations"
- // these are not private because they are needed
- // in the invoker (they are part of the environment
- // passed to the user container)
- val edgeHostName = "edge.host"
- val whiskVersionDate = "whisk.version.date"
- val whiskVersionBuildno = "whisk.version.buildno"
+ // these are not private because they are needed
+ // in the invoker (they are part of the environment
+ // passed to the user container)
+ val edgeHostName = "edge.host"
+ val whiskVersionDate = "whisk.version.date"
+ val whiskVersionBuildno = "whisk.version.buildno"
- val whiskVersion = Map(whiskVersionDate -> null, whiskVersionBuildno -> null)
+ val whiskVersion = Map(whiskVersionDate -> null, whiskVersionBuildno -> null)
- val dockerImagePrefix = "docker.image.prefix"
- val dockerImageTag = "docker.image.tag"
+ val dockerImagePrefix = "docker.image.prefix"
+ val dockerImageTag = "docker.image.tag"
- val invokerContainerNetwork = "invoker.container.network"
- val invokerContainerPolicy = "invoker.container.policy"
- val invokerContainerDns = "invoker.container.dns"
- val invokerNumCore = "invoker.numcore"
- val invokerCoreShare = "invoker.coreshare"
+ val invokerContainerNetwork = "invoker.container.network"
+ val invokerContainerPolicy = "invoker.container.policy"
+ val invokerContainerDns = "invoker.container.dns"
+ val invokerNumCore = "invoker.numcore"
+ val invokerCoreShare = "invoker.coreshare"
- val wskApiProtocol = "whisk.api.host.proto"
- val wskApiPort = "whisk.api.host.port"
- val wskApiHostname = "whisk.api.host.name"
- val wskApiHost = Map(wskApiProtocol -> "https", wskApiPort -> 443.toString, wskApiHostname -> null)
+ val wskApiProtocol = "whisk.api.host.proto"
+ val wskApiPort = "whisk.api.host.port"
+ val wskApiHostname = "whisk.api.host.name"
+ val wskApiHost = Map(wskApiProtocol -> "https", wskApiPort -> 443.toString, wskApiHostname -> null)
- val mainDockerEndpoint = "main.docker.endpoint"
+ val mainDockerEndpoint = "main.docker.endpoint"
- private val controllerBlackboxFraction = "controller.blackboxFraction"
- val controllerInstances = "controller.instances"
+ private val controllerBlackboxFraction = "controller.blackboxFraction"
+ val controllerInstances = "controller.instances"
- val loadbalancerInvokerBusyThreshold = "loadbalancer.invokerBusyThreshold"
+ val loadbalancerInvokerBusyThreshold = "loadbalancer.invokerBusyThreshold"
- val kafkaHostName = "kafka.host"
- private val zookeeperHostName = "zookeeper.host"
+ val kafkaHostName = "kafka.host"
+ private val zookeeperHostName = "zookeeper.host"
- private val edgeHostApiPort = "edge.host.apiport"
- val kafkaHostPort = "kafka.host.port"
- private val zookeeperHostPort = "zookeeper.host.port"
+ private val edgeHostApiPort = "edge.host.apiport"
+ val kafkaHostPort = "kafka.host.port"
+ private val zookeeperHostPort = "zookeeper.host.port"
- val invokerHostsList = "invoker.hosts"
+ val invokerHostsList = "invoker.hosts"
- val edgeHost = Map(edgeHostName -> null, edgeHostApiPort -> null)
- val invokerHosts = Map(invokerHostsList -> null)
- val kafkaHost = Map(kafkaHostName -> null, kafkaHostPort -> null)
+ val edgeHost = Map(edgeHostName -> null, edgeHostApiPort -> null)
+ val invokerHosts = Map(invokerHostsList -> null)
+ val kafkaHost = Map(kafkaHostName -> null, kafkaHostPort -> null)
- val runtimesManifest = "runtimes.manifest"
+ val runtimesManifest = "runtimes.manifest"
- val actionSequenceMaxLimit = "limits.actions.sequence.maxLength"
- val actionInvokePerMinuteLimit = "limits.actions.invokes.perMinute"
- val actionInvokeConcurrentLimit = "limits.actions.invokes.concurrent"
- val actionInvokeSystemOverloadLimit = "limits.actions.invokes.concurrentInSystem"
- val triggerFirePerMinuteLimit = "limits.triggers.fires.perMinute"
+ val actionSequenceMaxLimit = "limits.actions.sequence.maxLength"
+ val actionInvokePerMinuteLimit = "limits.actions.invokes.perMinute"
+ val actionInvokeConcurrentLimit = "limits.actions.invokes.concurrent"
+ val actionInvokeSystemOverloadLimit = "limits.actions.invokes.concurrentInSystem"
+ val triggerFirePerMinuteLimit = "limits.triggers.fires.perMinute"
}
diff --git a/common/scala/src/main/scala/whisk/core/connector/Message.scala b/common/scala/src/main/scala/whisk/core/connector/Message.scala
index 8da3d1a..3059f6b 100644
--- a/common/scala/src/main/scala/whisk/core/connector/Message.scala
+++ b/common/scala/src/main/scala/whisk/core/connector/Message.scala
@@ -31,90 +31,90 @@ import whisk.core.entity.WhiskActivation
/** Basic trait for messages that are sent on a message bus connector. */
trait Message {
- /**
- * A transaction id to attach to the message.
- */
- val transid = TransactionId.unknown
-
- /**
- * Serializes message to string. Must be idempotent.
- */
- def serialize: String
-
- /**
- * String representation of the message. Delegates to serialize.
- */
- override def toString = serialize
+
+ /**
+ * A transaction id to attach to the message.
+ */
+ val transid = TransactionId.unknown
+
+ /**
+ * Serializes message to string. Must be idempotent.
+ */
+ def serialize: String
+
+ /**
+ * String representation of the message. Delegates to serialize.
+ */
+ override def toString = serialize
}
-case class ActivationMessage(
- override val transid: TransactionId,
- action: FullyQualifiedEntityName,
- revision: DocRevision,
- user: Identity,
- activationId: ActivationId,
- activationNamespace: EntityPath,
- rootControllerIndex: InstanceId,
- blocking: Boolean,
- content: Option[JsObject],
- cause: Option[ActivationId] = None)
+case class ActivationMessage(override val transid: TransactionId,
+ action: FullyQualifiedEntityName,
+ revision: DocRevision,
+ user: Identity,
+ activationId: ActivationId,
+ activationNamespace: EntityPath,
+ rootControllerIndex: InstanceId,
+ blocking: Boolean,
+ content: Option[JsObject],
+ cause: Option[ActivationId] = None)
extends Message {
- def meta = JsObject("meta" -> {
- cause map {
- c => JsObject(c.toJsObject.fields ++ activationId.toJsObject.fields)
- } getOrElse {
- activationId.toJsObject
- }
+ def meta =
+ JsObject("meta" -> {
+ cause map { c =>
+ JsObject(c.toJsObject.fields ++ activationId.toJsObject.fields)
+ } getOrElse {
+ activationId.toJsObject
+ }
})
- override def serialize = ActivationMessage.serdes.write(this).compactPrint
+ override def serialize = ActivationMessage.serdes.write(this).compactPrint
- override def toString = {
- val value = (content getOrElse JsObject()).compactPrint
- s"$action?message=$value"
- }
+ override def toString = {
+ val value = (content getOrElse JsObject()).compactPrint
+ s"$action?message=$value"
+ }
- def causedBySequence: Boolean = cause.isDefined
+ def causedBySequence: Boolean = cause.isDefined
}
object ActivationMessage extends DefaultJsonProtocol {
- def parse(msg: String) = Try(serdes.read(msg.parseJson))
+ def parse(msg: String) = Try(serdes.read(msg.parseJson))
- private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
- implicit val serdes = jsonFormat10(ActivationMessage.apply)
+ private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
+ implicit val serdes = jsonFormat10(ActivationMessage.apply)
}
/**
* When adding fields, the serdes of the companion object must be updated also.
* The whisk activation field will have its logs stripped.
*/
-case class CompletionMessage(
- override val transid: TransactionId,
- response: Either[ActivationId, WhiskActivation],
- invoker: InstanceId)
+case class CompletionMessage(override val transid: TransactionId,
+ response: Either[ActivationId, WhiskActivation],
+ invoker: InstanceId)
extends Message {
- override def serialize: String = {
- CompletionMessage.serdes.write(this).compactPrint
- }
+ override def serialize: String = {
+ CompletionMessage.serdes.write(this).compactPrint
+ }
- override def toString = {
- response.fold(l => l, r => r.activationId).asString
- }
+ override def toString = {
+ response.fold(l => l, r => r.activationId).asString
+ }
}
object CompletionMessage extends DefaultJsonProtocol {
- def parse(msg: String): Try[CompletionMessage] = Try(serdes.read(msg.parseJson))
- private val serdes = jsonFormat3(CompletionMessage.apply)
+ def parse(msg: String): Try[CompletionMessage] = Try(serdes.read(msg.parseJson))
+ private val serdes = jsonFormat3(CompletionMessage.apply)
}
case class PingMessage(instance: InstanceId) extends Message {
- override def serialize = PingMessage.serdes.write(this).compactPrint
+ override def serialize = PingMessage.serdes.write(this).compactPrint
}
object PingMessage extends DefaultJsonProtocol {
- def parse(msg: String) = Try(serdes.read(msg.parseJson))
- implicit val serdes = jsonFormat(PingMessage.apply _, "name")
+ def parse(msg: String) = Try(serdes.read(msg.parseJson))
+ implicit val serdes = jsonFormat(PingMessage.apply _, "name")
}
diff --git a/common/scala/src/main/scala/whisk/core/connector/MessageConsumer.scala b/common/scala/src/main/scala/whisk/core/connector/MessageConsumer.scala
index f335441..31fcea0 100644
--- a/common/scala/src/main/scala/whisk/core/connector/MessageConsumer.scala
+++ b/common/scala/src/main/scala/whisk/core/connector/MessageConsumer.scala
@@ -33,47 +33,47 @@ import whisk.common.TransactionId
trait MessageConsumer {
- /** The maximum number of messages peeked (i.e., max number of messages retrieved during a long poll). */
- val maxPeek: Int
-
- /**
- * Gets messages via a long poll. May or may not remove messages
- * from the message connector. Use commit() to ensure messages are
- * removed from the connector.
- *
- * @param duration for the long poll
- * @return iterable collection (topic, partition, offset, bytes)
- */
- def peek(duration: Duration): Iterable[(String, Int, Long, Array[Byte])]
-
- /**
- * Commits offsets from last peek operation to ensure they are removed
- * from the connector.
- */
- def commit(): Unit
-
- /** Closes consumer. */
- def close(): Unit
+ /** The maximum number of messages peeked (i.e., max number of messages retrieved during a long poll). */
+ val maxPeek: Int
+
+ /**
+ * Gets messages via a long poll. May or may not remove messages
+ * from the message connector. Use commit() to ensure messages are
+ * removed from the connector.
+ *
+ * @param duration for the long poll
+ * @return iterable collection (topic, partition, offset, bytes)
+ */
+ def peek(duration: Duration): Iterable[(String, Int, Long, Array[Byte])]
+
+ /**
+ * Commits offsets from last peek operation to ensure they are removed
+ * from the connector.
+ */
+ def commit(): Unit
+
+ /** Closes consumer. */
+ def close(): Unit
}
object MessageFeed {
- protected sealed trait FeedState
- protected[connector] case object Idle extends FeedState
- protected[connector] case object FillingPipeline extends FeedState
- protected[connector] case object DrainingPipeline extends FeedState
+ protected sealed trait FeedState
+ protected[connector] case object Idle extends FeedState
+ protected[connector] case object FillingPipeline extends FeedState
+ protected[connector] case object DrainingPipeline extends FeedState
- protected sealed trait FeedData
- private case object NoData extends FeedData
+ protected sealed trait FeedData
+ private case object NoData extends FeedData
- /** Indicates the consumer is ready to accept messages from the message bus for processing. */
- object Ready
+ /** Indicates the consumer is ready to accept messages from the message bus for processing. */
+ object Ready
- /** Steady state message, indicates capacity in downstream process to receive more messages. */
- object Processed
+ /** Steady state message, indicates capacity in downstream process to receive more messages. */
+ object Processed
- /** Indicates the fill operation has completed. */
- private case class FillCompleted(messages: Seq[(String, Int, Long, Array[Byte])])
+ /** Indicates the fill operation has completed. */
+ private case class FillCompleted(messages: Seq[(String, Int, Long, Array[Byte])])
}
/**
@@ -92,140 +92,148 @@ object MessageFeed {
* of outstanding requests is below the pipeline fill threshold.
*/
@throws[IllegalArgumentException]
-class MessageFeed(
- description: String,
- logging: Logging,
- consumer: MessageConsumer,
- maximumHandlerCapacity: Int,
- longPollDuration: FiniteDuration,
- handler: Array[Byte] => Future[Unit],
- autoStart: Boolean = true,
- logHandoff: Boolean = true)
+class MessageFeed(description: String,
+ logging: Logging,
+ consumer: MessageConsumer,
+ maximumHandlerCapacity: Int,
+ longPollDuration: FiniteDuration,
+ handler: Array[Byte] => Future[Unit],
+ autoStart: Boolean = true,
+ logHandoff: Boolean = true)
extends FSM[MessageFeed.FeedState, MessageFeed.FeedData] {
- import MessageFeed._
-
- // double-buffer to make up for message bus read overhead
- val maxPipelineDepth = maximumHandlerCapacity * 2
- private val pipelineFillThreshold = maxPipelineDepth - consumer.maxPeek
-
- require(consumer.maxPeek <= maxPipelineDepth, "consumer may not yield more messages per peek than permitted by max depth")
-
- private val outstandingMessages = mutable.Queue[(String, Int, Long, Array[Byte])]()
- private var handlerCapacity = maximumHandlerCapacity
-
- private implicit val tid = TransactionId.dispatcher
-
- logging.info(this, s"handler capacity = $maximumHandlerCapacity, pipeline fill at = $pipelineFillThreshold, pipeline depth = $maxPipelineDepth")
-
- when(Idle) {
- case Event(Ready, _) =>
- fillPipeline()
- goto(FillingPipeline)
-
- case _ => stay
- }
-
- // wait for fill to complete, and keep filling if there is
- // capacity otherwise wait to drain
- when(FillingPipeline) {
- case Event(Processed, _) =>
- updateHandlerCapacity()
- sendOutstandingMessages()
- stay
-
- case Event(FillCompleted(messages), _) =>
- outstandingMessages.enqueue(messages: _*)
- sendOutstandingMessages()
-
- if (shouldFillQueue()) {
- fillPipeline()
- stay
- } else {
- goto(DrainingPipeline)
- }
-
- case _ => stay
- }
-
- when(DrainingPipeline) {
- case Event(Processed, _) =>
- updateHandlerCapacity()
- sendOutstandingMessages()
- if (shouldFillQueue()) {
- fillPipeline()
- goto(FillingPipeline)
- } else stay
-
- case _ => stay
- }
-
- onTransition { case _ -> Idle => if (autoStart) self ! Ready }
- startWith(Idle, MessageFeed.NoData)
- initialize()
-
- private implicit val ec = context.system.dispatcher
-
- private def fillPipeline(): Unit = {
- if (outstandingMessages.size <= pipelineFillThreshold) {
- Future {
- blocking {
- // Grab next batch of messages and commit offsets immediately
- // essentially marking the activation as having satisfied "at most once"
- // semantics (this is the point at which the activation is considered started).
- // If the commit fails, then messages peeked are peeked again on the next poll.
- // While the commit is synchronous and will block until it completes, at steady
- // state with enough buffering (i.e., maxPipelineDepth > maxPeek), the latency
- // of the commit should be masked.
- val records = consumer.peek(longPollDuration)
- consumer.commit()
- FillCompleted(records.toSeq)
- }
- }.andThen {
- case Failure(e: CommitFailedException) => logging.error(this, s"failed to commit $description consumer offset: $e")
- case Failure(e: Throwable) => logging.error(this, s"exception while pulling new $description records: $e")
- }.recover {
- case _ => FillCompleted(Seq.empty)
- }.pipeTo(self)
- } else {
- logging.error(this, s"dropping fill request until $description feed is drained")
+ import MessageFeed._
+
+ // double-buffer to make up for message bus read overhead
+ val maxPipelineDepth = maximumHandlerCapacity * 2
+ private val pipelineFillThreshold = maxPipelineDepth - consumer.maxPeek
+
+ require(
+ consumer.maxPeek <= maxPipelineDepth,
+ "consumer may not yield more messages per peek than permitted by max depth")
+
+ private val outstandingMessages = mutable.Queue[(String, Int, Long, Array[Byte])]()
+ private var handlerCapacity = maximumHandlerCapacity
+
+ private implicit val tid = TransactionId.dispatcher
+
+ logging.info(
+ this,
+ s"handler capacity = $maximumHandlerCapacity, pipeline fill at = $pipelineFillThreshold, pipeline depth = $maxPipelineDepth")
+
+ when(Idle) {
+ case Event(Ready, _) =>
+ fillPipeline()
+ goto(FillingPipeline)
+
+ case _ => stay
+ }
+
+ // wait for fill to complete, and keep filling if there is
+ // capacity otherwise wait to drain
+ when(FillingPipeline) {
+ case Event(Processed, _) =>
+ updateHandlerCapacity()
+ sendOutstandingMessages()
+ stay
+
+ case Event(FillCompleted(messages), _) =>
+ outstandingMessages.enqueue(messages: _*)
+ sendOutstandingMessages()
+
+ if (shouldFillQueue()) {
+ fillPipeline()
+ stay
+ } else {
+ goto(DrainingPipeline)
+ }
+
+ case _ => stay
+ }
+
+ when(DrainingPipeline) {
+ case Event(Processed, _) =>
+ updateHandlerCapacity()
+ sendOutstandingMessages()
+ if (shouldFillQueue()) {
+ fillPipeline()
+ goto(FillingPipeline)
+ } else stay
+
+ case _ => stay
+ }
+
+ onTransition { case _ -> Idle => if (autoStart) self ! Ready }
+ startWith(Idle, MessageFeed.NoData)
+ initialize()
+
+ private implicit val ec = context.system.dispatcher
+
+ private def fillPipeline(): Unit = {
+ if (outstandingMessages.size <= pipelineFillThreshold) {
+ Future {
+ blocking {
+ // Grab next batch of messages and commit offsets immediately
+ // essentially marking the activation as having satisfied "at most once"
+ // semantics (this is the point at which the activation is considered started).
+ // If the commit fails, then messages peeked are peeked again on the next poll.
+ // While the commit is synchronous and will block until it completes, at steady
+ // state with enough buffering (i.e., maxPipelineDepth > maxPeek), the latency
+ // of the commit should be masked.
+ val records = consumer.peek(longPollDuration)
+ consumer.commit()
+ FillCompleted(records.toSeq)
+ }
+ }.andThen {
+ case Failure(e: CommitFailedException) =>
+ logging.error(this, s"failed to commit $description consumer offset: $e")
+ case Failure(e: Throwable) => logging.error(this, s"exception while pulling new $description records: $e")
}
+ .recover {
+ case _ => FillCompleted(Seq.empty)
+ }
+ .pipeTo(self)
+ } else {
+ logging.error(this, s"dropping fill request until $description feed is drained")
}
+ }
- /** Send as many messages as possible to the handler. */
- @tailrec
- private def sendOutstandingMessages(): Unit = {
- val occupancy = outstandingMessages.size
- if (occupancy > 0 && handlerCapacity > 0) {
- val (topic, partition, offset, bytes) = outstandingMessages.dequeue()
+ /** Send as many messages as possible to the handler. */
+ @tailrec
+ private def sendOutstandingMessages(): Unit = {
+ val occupancy = outstandingMessages.size
+ if (occupancy > 0 && handlerCapacity > 0) {
+ val (topic, partition, offset, bytes) = outstandingMessages.dequeue()
- if (logHandoff) logging.info(this, s"processing $topic[$partition][$offset] ($occupancy/$handlerCapacity)")
- handler(bytes)
- handlerCapacity -= 1
+ if (logHandoff) logging.info(this, s"processing $topic[$partition][$offset] ($occupancy/$handlerCapacity)")
+ handler(bytes)
+ handlerCapacity -= 1
- sendOutstandingMessages()
- }
+ sendOutstandingMessages()
}
-
- private def shouldFillQueue(): Boolean = {
- val occupancy = outstandingMessages.size
- if (occupancy <= pipelineFillThreshold) {
- logging.debug(this, s"$description pipeline has capacity: $occupancy <= $pipelineFillThreshold ($handlerCapacity)")
- true
- } else {
- logging.debug(this, s"$description pipeline must drain: $occupancy > $pipelineFillThreshold")
- false
- }
+ }
+
+ private def shouldFillQueue(): Boolean = {
+ val occupancy = outstandingMessages.size
+ if (occupancy <= pipelineFillThreshold) {
+ logging.debug(
+ this,
+ s"$description pipeline has capacity: $occupancy <= $pipelineFillThreshold ($handlerCapacity)")
+ true
+ } else {
+ logging.debug(this, s"$description pipeline must drain: $occupancy > $pipelineFillThreshold")
+ false
}
+ }
- private def updateHandlerCapacity(): Int = {
- logging.debug(self, s"$description received processed msg, current capacity = $handlerCapacity")
+ private def updateHandlerCapacity(): Int = {
+ logging.debug(self, s"$description received processed msg, current capacity = $handlerCapacity")
- if (handlerCapacity < maximumHandlerCapacity) {
- handlerCapacity += 1
- handlerCapacity
- } else {
- if (handlerCapacity > maximumHandlerCapacity) logging.error(self, s"$description capacity already at max")
- maximumHandlerCapacity
- }
+ if (handlerCapacity < maximumHandlerCapacity) {
+ handlerCapacity += 1
+ handlerCapacity
+ } else {
+ if (handlerCapacity > maximumHandlerCapacity) logging.error(self, s"$description capacity already at max")
+ maximumHandlerCapacity
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/connector/MessageProducer.scala b/common/scala/src/main/scala/whisk/core/connector/MessageProducer.scala
index 4c89711..53d8e2f 100644
--- a/common/scala/src/main/scala/whisk/core/connector/MessageProducer.scala
+++ b/common/scala/src/main/scala/whisk/core/connector/MessageProducer.scala
@@ -22,12 +22,13 @@ import scala.concurrent.Future
import org.apache.kafka.clients.producer.RecordMetadata
trait MessageProducer {
- /** Count of messages sent. */
- def sentCount(): Long
- /** Sends msg to topic. This is an asynchronous operation. */
- def send(topic: String, msg: Message): Future[RecordMetadata]
+ /** Count of messages sent. */
+ def sentCount(): Long
- /** Closes producer. */
- def close(): Unit
+ /** Sends msg to topic. This is an asynchronous operation. */
+ def send(topic: String, msg: Message): Future[RecordMetadata]
+
+ /** Closes producer. */
+ def close(): Unit
}
diff --git a/common/scala/src/main/scala/whisk/core/connector/MessagingProvider.scala b/common/scala/src/main/scala/whisk/core/connector/MessagingProvider.scala
index b88e8d9..ec938e1 100644
--- a/common/scala/src/main/scala/whisk/core/connector/MessagingProvider.scala
+++ b/common/scala/src/main/scala/whisk/core/connector/MessagingProvider.scala
@@ -28,6 +28,10 @@ import whisk.spi.Spi
* An Spi for providing Messaging implementations.
*/
trait MessagingProvider extends Spi {
- def getConsumer(config: WhiskConfig, groupId: String, topic: String, maxPeek: Int = Int.MaxValue, maxPollInterval: FiniteDuration = 5.minutes)(implicit logging: Logging): MessageConsumer
- def getProducer(config: WhiskConfig, ec: ExecutionContext)(implicit logging: Logging): MessageProducer
+ def getConsumer(config: WhiskConfig,
+ groupId: String,
+ topic: String,
+ maxPeek: Int = Int.MaxValue,
+ maxPollInterval: FiniteDuration = 5.minutes)(implicit logging: Logging): MessageConsumer
+ def getProducer(config: WhiskConfig, ec: ExecutionContext)(implicit logging: Logging): MessageProducer
}
diff --git a/common/scala/src/main/scala/whisk/core/database/ArtifactStore.scala b/common/scala/src/main/scala/whisk/core/database/ArtifactStore.scala
index fc0638b..2a61730 100644
--- a/common/scala/src/main/scala/whisk/core/database/ArtifactStore.scala
+++ b/common/scala/src/main/scala/whisk/core/database/ArtifactStore.scala
@@ -31,82 +31,88 @@ import whisk.core.entity.DocInfo
abstract class StaleParameter(val value: Option[String])
object StaleParameter {
- case object Ok extends StaleParameter(Some("ok"))
- case object UpdateAfter extends StaleParameter(Some("update_after"))
- case object No extends StaleParameter(None)
+ case object Ok extends StaleParameter(Some("ok"))
+ case object UpdateAfter extends StaleParameter(Some("update_after"))
+ case object No extends StaleParameter(None)
}
/** Basic client to put and delete artifacts in a data store. */
trait ArtifactStore[DocumentAbstraction] {
- /** Execution context for futures */
- protected[core] implicit val executionContext: ExecutionContext
+ /** Execution context for futures */
+ protected[core] implicit val executionContext: ExecutionContext
- implicit val logging: Logging
+ implicit val logging: Logging
- /**
- * Puts (saves) document to database using a future.
- * If the operation is successful, the future completes with DocId else an appropriate exception.
- *
- * @param d the document to put in the database
- * @param transid the transaction id for logging
- * @return a future that completes either with DocId
- */
- protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo]
+ /**
+ * Puts (saves) document to database using a future.
+ * If the operation is successful, the future completes with DocId else an appropriate exception.
+ *
+ * @param d the document to put in the database
+ * @param transid the transaction id for logging
+ * @return a future that completes either with DocId
+ */
+ protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo]
- /**
- * Deletes document from database using a future.
- * If the operation is successful, the future completes with true.
- *
- * @param doc the document info for the record to delete (must contain valid id and rev)
- * @param transid the transaction id for logging
- * @return a future that completes true iff the document is deleted, else future is failed
- */
- protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean]
+ /**
+ * Deletes document from database using a future.
+ * If the operation is successful, the future completes with true.
+ *
+ * @param doc the document info for the record to delete (must contain valid id and rev)
+ * @param transid the transaction id for logging
+ * @return a future that completes true iff the document is deleted, else future is failed
+ */
+ protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean]
- /**
- * Gets document from database by id using a future.
- * If the operation is successful, the future completes with the requested document if it exists.
- *
- * @param doc the document info for the record to get (must contain valid id and rev)
- * @param transid the transaction id for logging
- * @param ma manifest for A to determine its runtime type, required by some db APIs
- * @return a future that completes either with DocumentAbstraction if the document exists and is deserializable into desired type
- */
- protected[database] def get[A <: DocumentAbstraction](doc: DocInfo)(
- implicit transid: TransactionId,
- ma: Manifest[A]): Future[A]
+ /**
+ * Gets document from database by id using a future.
+ * If the operation is successful, the future completes with the requested document if it exists.
+ *
+ * @param doc the document info for the record to get (must contain valid id and rev)
+ * @param transid the transaction id for logging
+ * @param ma manifest for A to determine its runtime type, required by some db APIs
+ * @return a future that completes either with DocumentAbstraction if the document exists and is deserializable into desired type
+ */
+ protected[database] def get[A <: DocumentAbstraction](doc: DocInfo)(implicit transid: TransactionId,
+ ma: Manifest[A]): Future[A]
- /**
- * Gets all documents from database view that match a start key, up to an end key, using a future.
- * If the operation is successful, the promise completes with List[View]] with zero or more documents.
- *
- * @param table the name of the table to query
- * @param startKey to starting key to query the view for
- * @param endKey to starting key to query the view for
- * @param skip the number of record to skip (for pagination)
- * @param limit the maximum number of records matching the key to return, iff > 0
- * @param includeDocs include full documents matching query iff true (shall not be used with reduce)
- * @param descending reverse results iff true
- * @param reduce apply reduction associated with query to the result iff true
- * @param transid the transaction id for logging
- * @return a future that completes with List[JsObject] of all documents from view between start and end key (list may be empty)
- */
- protected[core] def query(table: String, startKey: List[Any], endKey: List[Any], skip: Int, limit: Int, includeDocs: Boolean, descending: Boolean, reduce: Boolean, stale: StaleParameter)(
- implicit transid: TransactionId): Future[List[JsObject]]
+ /**
+ * Gets all documents from database view that match a start key, up to an end key, using a future.
+ * If the operation is successful, the promise completes with List[View]] with zero or more documents.
+ *
+ * @param table the name of the table to query
+ * @param startKey to starting key to query the view for
+ * @param endKey to starting key to query the view for
+ * @param skip the number of record to skip (for pagination)
+ * @param limit the maximum number of records matching the key to return, iff > 0
+ * @param includeDocs include full documents matching query iff true (shall not be used with reduce)
+ * @param descending reverse results iff true
+ * @param reduce apply reduction associated with query to the result iff true
+ * @param transid the transaction id for logging
+ * @return a future that completes with List[JsObject] of all documents from view between start and end key (list may be empty)
+ */
+ protected[core] def query(table: String,
+ startKey: List[Any],
+ endKey: List[Any],
+ skip: Int,
+ limit: Int,
+ includeDocs: Boolean,
+ descending: Boolean,
+ reduce: Boolean,
+ stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]]
- /**
- * Attaches a "file" of type `contentType` to an existing document. The revision for the document must be set.
- */
- protected[core] def attach(doc: DocInfo, name: String, contentType: ContentType, docStream: Source[ByteString, _])(
- implicit transid: TransactionId): Future[DocInfo]
+ /**
+ * Attaches a "file" of type `contentType` to an existing document. The revision for the document must be set.
+ */
+ protected[core] def attach(doc: DocInfo, name: String, contentType: ContentType, docStream: Source[ByteString, _])(
+ implicit transid: TransactionId): Future[DocInfo]
- /**
- * Retrieves a saved attachment, streaming it into the provided Sink.
- */
- protected[core] def readAttachment[T](doc: DocInfo, name: String, sink: Sink[ByteString, Future[T]])(
- implicit transid: TransactionId): Future[(ContentType, T)]
+ /**
+ * Retrieves a saved attachment, streaming it into the provided Sink.
+ */
+ protected[core] def readAttachment[T](doc: DocInfo, name: String, sink: Sink[ByteString, Future[T]])(
+ implicit transid: TransactionId): Future[(ContentType, T)]
- /** Shut it down. After this invocation, every other call is invalid. */
- def shutdown(): Unit
+ /** Shut it down. After this invocation, every other call is invalid. */
+ def shutdown(): Unit
}
diff --git a/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala b/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
index 5234fc8..ba471aa 100644
--- a/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
+++ b/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
@@ -26,10 +26,9 @@ import whisk.spi.Spi
/**
* An Spi for providing ArtifactStore implementations
*/
-
trait ArtifactStoreProvider extends Spi {
- def makeStore[D <: DocumentSerializer](config: WhiskConfig, name: WhiskConfig => String)(
- implicit jsonFormat: RootJsonFormat[D],
- actorSystem: ActorSystem,
- logging: Logging): ArtifactStore[D]
+ def makeStore[D <: DocumentSerializer](config: WhiskConfig, name: WhiskConfig => String)(
+ implicit jsonFormat: RootJsonFormat[D],
+ actorSystem: ActorSystem,
+ logging: Logging): ArtifactStore[D]
}
diff --git a/common/scala/src/main/scala/whisk/core/database/CloudantRestClient.scala b/common/scala/src/main/scala/whisk/core/database/CloudantRestClient.scala
index 1295bb1..29ec17e 100644
--- a/common/scala/src/main/scala/whisk/core/database/CloudantRestClient.scala
+++ b/common/scala/src/main/scala/whisk/core/database/CloudantRestClient.scala
@@ -30,11 +30,13 @@ import whisk.common.Logging
* This class only handles the basic communication to the proper endpoints
* ("JSON in, JSON out"). It is up to its clients to interpret the results.
*/
-class CloudantRestClient(host: String, port: Int, username: String, password: String, db: String)(implicit system: ActorSystem, logging: Logging)
+class CloudantRestClient(host: String, port: Int, username: String, password: String, db: String)(
+ implicit system: ActorSystem,
+ logging: Logging)
extends CouchDbRestClient("https", host, port, username, password, db) {
- // https://cloudant.com/blog/cloudant-query-grows-up-to-handle-ad-hoc-queries/#.VvllCD-0z2C
- def simpleQuery(doc: JsObject): Future[Either[StatusCode, JsObject]] = {
- requestJson[JsObject](mkJsonRequest(HttpMethods.POST, uri(db, "_find"), doc))
- }
+ // https://cloudant.com/blog/cloudant-query-grows-up-to-handle-ad-hoc-queries/#.VvllCD-0z2C
+ def simpleQuery(doc: JsObject): Future[Either[StatusCode, JsObject]] = {
+ requestJson[JsObject](mkJsonRequest(HttpMethods.POST, uri(db, "_find"), doc))
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/database/CouchDbRestClient.scala b/common/scala/src/main/scala/whisk/core/database/CouchDbRestClient.scala
index dfca680..0729d00 100644
--- a/common/scala/src/main/scala/whisk/core/database/CouchDbRestClient.scala
+++ b/common/scala/src/main/scala/whisk/core/database/CouchDbRestClient.scala
@@ -22,7 +22,7 @@ import java.nio.charset.StandardCharsets
import scala.concurrent.Future
import scala.concurrent.Promise
-import scala.util.{ Success, Failure }
+import scala.util.{Failure, Success}
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
@@ -48,214 +48,236 @@ import whisk.common.Logging
* up the pool corresponding to the host. It is also easier to add an extra
* queueing mechanism.
*/
-class CouchDbRestClient(protocol: String, host: String, port: Int, username: String, password: String, db: String)(implicit system: ActorSystem, logging: Logging) {
- require(protocol == "http" || protocol == "https", "Protocol must be one of { http, https }.")
-
- private implicit val context = system.dispatcher
- private implicit val materializer = ActorMaterializer()
-
- // Creates or retrieves a connection pool for the host.
- private val pool = if (protocol == "http") {
- Http().cachedHostConnectionPool[Promise[HttpResponse]](host = host, port = port)
- } else {
- Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](host = host, port = port)
- }
-
- private val poolPromise = Promise[HostConnectionPool]
-
- // Additional queue in case all connections are busy. Should hardly ever be
- // filled in practice but can be useful, e.g., in tests starting many
- // asynchronous requests in a very short period of time.
- private val QUEUE_SIZE = 16 * 1024;
- private val requestQueue = Source.queue(QUEUE_SIZE, OverflowStrategy.dropNew)
- .via(pool.mapMaterializedValue { x => poolPromise.success(x); x })
- .toMat(Sink.foreach({
- case ((Success(response), p)) => p.success(response)
- case ((Failure(error), p)) => p.failure(error)
- }))(Keep.left)
- .run
-
- // Properly encodes the potential slashes in each segment.
- protected def uri(segments: Any*): Uri = {
- val encodedSegments = segments.map(s => URLEncoder.encode(s.toString, StandardCharsets.UTF_8.name))
- Uri(s"/${encodedSegments.mkString("/")}")
- }
-
- // Headers common to all requests.
- private val baseHeaders = List(
- Authorization(BasicHttpCredentials(username, password)),
- Accept(MediaTypes.`application/json`))
-
- // Prepares a request with the proper headers.
- private def mkRequest0(
- method: HttpMethod,
- uri: Uri,
- body: Future[MessageEntity],
- forRev: Option[String] = None): Future[HttpRequest] = {
- val revHeader = forRev.map(r => `If-Match`(EntityTagRange(EntityTag(r)))).toList
- val headers = revHeader ::: baseHeaders
- body.map { b => HttpRequest(method = method, uri = uri, headers = headers, entity = b) }
- }
-
- protected def mkRequest(method: HttpMethod, uri: Uri, forRev: Option[String] = None): Future[HttpRequest] = {
- mkRequest0(method, uri, Future.successful(HttpEntity.Empty), forRev = forRev)
- }
-
- protected def mkJsonRequest(method: HttpMethod, uri: Uri, body: JsValue, forRev: Option[String] = None): Future[HttpRequest] = {
- val b = Marshal(body).to[MessageEntity]
- mkRequest0(method, uri, b, forRev = forRev)
+class CouchDbRestClient(protocol: String, host: String, port: Int, username: String, password: String, db: String)(
+ implicit system: ActorSystem,
+ logging: Logging) {
+ require(protocol == "http" || protocol == "https", "Protocol must be one of { http, https }.")
+
+ private implicit val context = system.dispatcher
+ private implicit val materializer = ActorMaterializer()
+
+ // Creates or retrieves a connection pool for the host.
+ private val pool = if (protocol == "http") {
+ Http().cachedHostConnectionPool[Promise[HttpResponse]](host = host, port = port)
+ } else {
+ Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](host = host, port = port)
+ }
+
+ private val poolPromise = Promise[HostConnectionPool]
+
+ // Additional queue in case all connections are busy. Should hardly ever be
+ // filled in practice but can be useful, e.g., in tests starting many
+ // asynchronous requests in a very short period of time.
+ private val QUEUE_SIZE = 16 * 1024;
+ private val requestQueue = Source
+ .queue(QUEUE_SIZE, OverflowStrategy.dropNew)
+ .via(pool.mapMaterializedValue { x =>
+ poolPromise.success(x); x
+ })
+ .toMat(Sink.foreach({
+ case ((Success(response), p)) => p.success(response)
+ case ((Failure(error), p)) => p.failure(error)
+ }))(Keep.left)
+ .run
+
+ // Properly encodes the potential slashes in each segment.
+ protected def uri(segments: Any*): Uri = {
+ val encodedSegments = segments.map(s => URLEncoder.encode(s.toString, StandardCharsets.UTF_8.name))
+ Uri(s"/${encodedSegments.mkString("/")}")
+ }
+
+ // Headers common to all requests.
+ private val baseHeaders =
+ List(Authorization(BasicHttpCredentials(username, password)), Accept(MediaTypes.`application/json`))
+
+ // Prepares a request with the proper headers.
+ private def mkRequest0(method: HttpMethod,
+ uri: Uri,
+ body: Future[MessageEntity],
+ forRev: Option[String] = None): Future[HttpRequest] = {
+ val revHeader = forRev.map(r => `If-Match`(EntityTagRange(EntityTag(r)))).toList
+ val headers = revHeader ::: baseHeaders
+ body.map { b =>
+ HttpRequest(method = method, uri = uri, headers = headers, entity = b)
}
-
- // Enqueue a request, and return a future capturing the corresponding response.
- // WARNING: make sure that if the future response is not failed, its entity
- // be drained entirely or the connection will be kept open until timeouts kick in.
- private def request0(futureRequest: Future[HttpRequest]): Future[HttpResponse] = {
- futureRequest flatMap { request =>
- val promise = Promise[HttpResponse]
-
- // When the future completes, we know whether the request made it
- // through the queue.
- requestQueue.offer(request -> promise).flatMap { buffered =>
- buffered match {
- case QueueOfferResult.Enqueued =>
- promise.future
-
- case QueueOfferResult.Dropped =>
- Future.failed(new Exception("DB request queue is full."))
-
- case QueueOfferResult.QueueClosed =>
- Future.failed(new Exception("DB request queue was closed."))
-
- case QueueOfferResult.Failure(f) =>
- Future.failed(f)
- }
- }
+ }
+
+ protected def mkRequest(method: HttpMethod, uri: Uri, forRev: Option[String] = None): Future[HttpRequest] = {
+ mkRequest0(method, uri, Future.successful(HttpEntity.Empty), forRev = forRev)
+ }
+
+ protected def mkJsonRequest(method: HttpMethod,
+ uri: Uri,
+ body: JsValue,
+ forRev: Option[String] = None): Future[HttpRequest] = {
+ val b = Marshal(body).to[MessageEntity]
+ mkRequest0(method, uri, b, forRev = forRev)
+ }
+
+ // Enqueue a request, and return a future capturing the corresponding response.
+ // WARNING: make sure that if the future response is not failed, its entity
+ // be drained entirely or the connection will be kept open until timeouts kick in.
+ private def request0(futureRequest: Future[HttpRequest]): Future[HttpResponse] = {
+ futureRequest flatMap { request =>
+ val promise = Promise[HttpResponse]
+
+ // When the future completes, we know whether the request made it
+ // through the queue.
+ requestQueue.offer(request -> promise).flatMap { buffered =>
+ buffered match {
+ case QueueOfferResult.Enqueued =>
+ promise.future
+
+ case QueueOfferResult.Dropped =>
+ Future.failed(new Exception("DB request queue is full."))
+
+ case QueueOfferResult.QueueClosed =>
+ Future.failed(new Exception("DB request queue was closed."))
+
+ case QueueOfferResult.Failure(f) =>
+ Future.failed(f)
}
+ }
}
-
- // Runs a request and returns either a JsObject, or a StatusCode if not 2xx.
- protected def requestJson[T: RootJsonReader](futureRequest: Future[HttpRequest]): Future[Either[StatusCode, T]] = {
- request0(futureRequest) flatMap { response =>
- if (response.status.isSuccess()) {
- Unmarshal(response.entity.withoutSizeLimit()).to[T].map { o => Right(o) }
- } else {
- // This is important, as it drains the entity stream.
- // Otherwise the connection stays open and the pool dries up.
- response.entity.withoutSizeLimit().dataBytes.runWith(Sink.ignore).map { _ => Left(response.status) }
- }
+ }
+
+ // Runs a request and returns either a JsObject, or a StatusCode if not 2xx.
+ protected def requestJson[T: RootJsonReader](futureRequest: Future[HttpRequest]): Future[Either[StatusCode, T]] = {
+ request0(futureRequest) flatMap { response =>
+ if (response.status.isSuccess()) {
+ Unmarshal(response.entity.withoutSizeLimit()).to[T].map { o =>
+ Right(o)
}
- }
-
- import spray.json.DefaultJsonProtocol._
-
- // http://docs.couchdb.org/en/1.6.1/api/document/common.html#put--db-docid
- def putDoc(id: String, doc: JsObject): Future[Either[StatusCode, JsObject]] =
- requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri(db, id), doc))
-
- // http://docs.couchdb.org/en/1.6.1/api/document/common.html#put--db-docid
- def putDoc(id: String, rev: String, doc: JsObject): Future[Either[StatusCode, JsObject]] =
- requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri(db, id), doc, forRev = Some(rev)))
-
- // http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid
- def getDoc(id: String): Future[Either[StatusCode, JsObject]] =
- requestJson[JsObject](mkRequest(HttpMethods.GET, uri(db, id)))
-
- // http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid
- def getDoc(id: String, rev: String): Future[Either[StatusCode, JsObject]] =
- requestJson[JsObject](mkRequest(HttpMethods.GET, uri(db, id), forRev = Some(rev)))
-
- // http://docs.couchdb.org/en/1.6.1/api/document/common.html#delete--db-docid
- def deleteDoc(id: String, rev: String): Future[Either[StatusCode, JsObject]] =
- requestJson[JsObject](mkRequest(HttpMethods.DELETE, uri(db, id), forRev = Some(rev)))
-
- // http://docs.couchdb.org/en/1.6.1/api/ddoc/views.html
- def executeView(designDoc: String, viewName: String)(
- startKey: List[Any] = Nil,
- endKey: List[Any] = Nil,
- skip: Option[Int] = None,
- limit: Option[Int] = None,
- stale: StaleParameter = StaleParameter.No,
- includeDocs: Boolean = false,
- descending: Boolean = false,
- reduce: Boolean = false,
- group: Boolean = false): Future[Either[StatusCode, JsObject]] = {
-
- require(reduce || !group, "Parameter 'group=true' cannot be used together with the parameter 'reduce=false'.")
-
- def any2json(any: Any): JsValue = any match {
- case b: Boolean => JsBoolean(b)
- case i: Int => JsNumber(i)
- case l: Long => JsNumber(l)
- case d: Double => JsNumber(d)
- case f: Float => JsNumber(f)
- case s: String => JsString(s)
- case _ =>
- logging.warn(this, s"Serializing uncontrolled type '${any.getClass}' to string in JSON conversion ('${any.toString}').")
- JsString(any.toString)
+ } else {
+ // This is important, as it drains the entity stream.
+ // Otherwise the connection stays open and the pool dries up.
+ response.entity.withoutSizeLimit().dataBytes.runWith(Sink.ignore).map { _ =>
+ Left(response.status)
}
-
- def list2OptJson(lst: List[Any]): Option[JsValue] = {
- lst match {
- case Nil => None
- case _ => Some(JsArray(lst.map(any2json): _*))
- }
- }
-
- val args = Seq[(String, Option[String])](
- "startkey" -> list2OptJson(startKey).map(_.toString),
- "endkey" -> list2OptJson(endKey).map(_.toString),
- "skip" -> skip.filter(_ > 0).map(_.toString),
- "limit" -> limit.filter(_ > 0).map(_.toString),
- "stale" -> stale.value,
- "include_docs" -> Some(includeDocs).filter(identity).map(_.toString),
- "descending" -> Some(descending).filter(identity).map(_.toString),
- "reduce" -> Some(reduce).map(_.toString),
- "group" -> Some(group).filter(identity).map(_.toString))
-
- // Throw out all undefined arguments.
- val argMap: Map[String, String] = args.collect({
- case (l, Some(r)) => (l, r)
- }).toMap
-
- val viewUri = uri(db, "_design", designDoc, "_view", viewName).withQuery(Uri.Query(argMap))
-
- requestJson[JsObject](mkRequest(HttpMethods.GET, viewUri))
+ }
}
-
- // Streams an attachment to the database
- // http://docs.couchdb.org/en/1.6.1/api/document/attachments.html#put--db-docid-attname
- def putAttachment(id: String, rev: String, attName: String, contentType: ContentType, source: Source[ByteString, _]): Future[Either[StatusCode, JsObject]] = {
- val entity = HttpEntity.Chunked(contentType, source.map(bs => HttpEntity.ChunkStreamPart(bs)))
- val request = mkRequest0(HttpMethods.PUT, uri(db, id, attName), Future.successful(entity), forRev = Some(rev))
- requestJson[JsObject](request)
+ }
+
+ import spray.json.DefaultJsonProtocol._
+
+ // http://docs.couchdb.org/en/1.6.1/api/document/common.html#put--db-docid
+ def putDoc(id: String, doc: JsObject): Future[Either[StatusCode, JsObject]] =
+ requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri(db, id), doc))
+
+ // http://docs.couchdb.org/en/1.6.1/api/document/common.html#put--db-docid
+ def putDoc(id: String, rev: String, doc: JsObject): Future[Either[StatusCode, JsObject]] =
+ requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri(db, id), doc, forRev = Some(rev)))
+
+ // http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid
+ def getDoc(id: String): Future[Either[StatusCode, JsObject]] =
+ requestJson[JsObject](mkRequest(HttpMethods.GET, uri(db, id)))
+
+ // http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid
+ def getDoc(id: String, rev: String): Future[Either[StatusCode, JsObject]] =
+ requestJson[JsObject](mkRequest(HttpMethods.GET, uri(db, id), forRev = Some(rev)))
+
+ // http://docs.couchdb.org/en/1.6.1/api/document/common.html#delete--db-docid
+ def deleteDoc(id: String, rev: String): Future[Either[StatusCode, JsObject]] =
+ requestJson[JsObject](mkRequest(HttpMethods.DELETE, uri(db, id), forRev = Some(rev)))
+
+ // http://docs.couchdb.org/en/1.6.1/api/ddoc/views.html
+ def executeView(designDoc: String, viewName: String)(startKey: List[Any] = Nil,
+ endKey: List[Any] = Nil,
+ skip: Option[Int] = None,
+ limit: Option[Int] = None,
+ stale: StaleParameter = StaleParameter.No,
+ includeDocs: Boolean = false,
+ descending: Boolean = false,
+ reduce: Boolean = false,
+ group: Boolean = false): Future[Either[StatusCode, JsObject]] = {
+
+ require(reduce || !group, "Parameter 'group=true' cannot be used together with the parameter 'reduce=false'.")
+
+ def any2json(any: Any): JsValue = any match {
+ case b: Boolean => JsBoolean(b)
+ case i: Int => JsNumber(i)
+ case l: Long => JsNumber(l)
+ case d: Double => JsNumber(d)
+ case f: Float => JsNumber(f)
+ case s: String => JsString(s)
+ case _ =>
+ logging.warn(
+ this,
+ s"Serializing uncontrolled type '${any.getClass}' to string in JSON conversion ('${any.toString}').")
+ JsString(any.toString)
}
- // Retrieves and streams an attachment into a Sink, producing a result of type T.
- // http://docs.couchdb.org/en/1.6.1/api/document/attachments.html#get--db-docid-attname
- def getAttachment[T](id: String, rev: String, attName: String, sink: Sink[ByteString, Future[T]]): Future[Either[StatusCode, (ContentType, T)]] = {
- val request = mkRequest(HttpMethods.GET, uri(db, id, attName), forRev = Some(rev))
-
- request0(request) flatMap { response =>
- if (response.status.isSuccess()) {
- response.entity.withoutSizeLimit().dataBytes.runWith(sink).map(r => Right(response.entity.contentType, r))
- } else {
- response.entity.withoutSizeLimit().dataBytes.runWith(Sink.ignore).map(_ => Left(response.status))
- }
- }
+ def list2OptJson(lst: List[Any]): Option[JsValue] = {
+ lst match {
+ case Nil => None
+ case _ => Some(JsArray(lst.map(any2json): _*))
+ }
}
- def shutdown(): Future[Unit] = {
- materializer.shutdown()
- // The code below shuts down the pool, but is apparently not tolerant
- // to multiple clients shutting down the same pool (the second one just
- // hangs). Given that shutdown is only relevant for tests (unused pools
- // close themselves anyway after some time) and that they can call
- // Http().shutdownAllConnectionPools(), this is not a major issue.
- /* Reintroduce below if they ever make HostConnectionPool.shutdown()
- * safe to call >1x.
- * val poolOpt = poolPromise.future.value.map(_.toOption).flatten
- * poolOpt.map(_.shutdown().map(_ => ())).getOrElse(Future.successful(()))
- */
- Future.successful(())
+ val args = Seq[(String, Option[String])](
+ "startkey" -> list2OptJson(startKey).map(_.toString),
+ "endkey" -> list2OptJson(endKey).map(_.toString),
+ "skip" -> skip.filter(_ > 0).map(_.toString),
+ "limit" -> limit.filter(_ > 0).map(_.toString),
+ "stale" -> stale.value,
+ "include_docs" -> Some(includeDocs).filter(identity).map(_.toString),
+ "descending" -> Some(descending).filter(identity).map(_.toString),
+ "reduce" -> Some(reduce).map(_.toString),
+ "group" -> Some(group).filter(identity).map(_.toString))
+
+ // Throw out all undefined arguments.
+ val argMap: Map[String, String] = args
+ .collect({
+ case (l, Some(r)) => (l, r)
+ })
+ .toMap
+
+ val viewUri = uri(db, "_design", designDoc, "_view", viewName).withQuery(Uri.Query(argMap))
+
+ requestJson[JsObject](mkRequest(HttpMethods.GET, viewUri))
+ }
+
+ // Streams an attachment to the database
+ // http://docs.couchdb.org/en/1.6.1/api/document/attachments.html#put--db-docid-attname
+ def putAttachment(id: String,
+ rev: String,
+ attName: String,
+ contentType: ContentType,
+ source: Source[ByteString, _]): Future[Either[StatusCode, JsObject]] = {
+ val entity = HttpEntity.Chunked(contentType, source.map(bs => HttpEntity.ChunkStreamPart(bs)))
+ val request = mkRequest0(HttpMethods.PUT, uri(db, id, attName), Future.successful(entity), forRev = Some(rev))
+ requestJson[JsObject](request)
+ }
+
+ // Retrieves and streams an attachment into a Sink, producing a result of type T.
+ // http://docs.couchdb.org/en/1.6.1/api/document/attachments.html#get--db-docid-attname
+ def getAttachment[T](id: String,
+ rev: String,
+ attName: String,
+ sink: Sink[ByteString, Future[T]]): Future[Either[StatusCode, (ContentType, T)]] = {
+ val request = mkRequest(HttpMethods.GET, uri(db, id, attName), forRev = Some(rev))
+
+ request0(request) flatMap { response =>
+ if (response.status.isSuccess()) {
+ response.entity.withoutSizeLimit().dataBytes.runWith(sink).map(r => Right(response.entity.contentType, r))
+ } else {
+ response.entity.withoutSizeLimit().dataBytes.runWith(Sink.ignore).map(_ => Left(response.status))
+ }
}
+ }
+
+ def shutdown(): Future[Unit] = {
+ materializer.shutdown()
+ // The code below shuts down the pool, but is apparently not tolerant
+ // to multiple clients shutting down the same pool (the second one just
+ // hangs). Given that shutdown is only relevant for tests (unused pools
+ // close themselves anyway after some time) and that they can call
+ // Http().shutdownAllConnectionPools(), this is not a major issue.
+ /* Reintroduce below if they ever make HostConnectionPool.shutdown()
+ * safe to call >1x.
+ * val poolOpt = poolPromise.future.value.map(_.toOption).flatten
+ * poolOpt.map(_.shutdown().map(_ => ())).getOrElse(Future.successful(()))
+ */
+ Future.successful(())
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala b/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
index 34feccb..3389274 100644
--- a/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
+++ b/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
@@ -47,249 +47,323 @@ import whisk.http.Messages
* @param serializerEvidence confirms the document abstraction is serializable to a Document with an id
*/
class CouchDbRestStore[DocumentAbstraction <: DocumentSerializer](
- dbProtocol: String,
- dbHost: String,
- dbPort: Int,
- dbUsername: String,
- dbPassword: String,
- dbName: String)(implicit system: ActorSystem, val logging: Logging, jsonFormat: RootJsonFormat[DocumentAbstraction])
+ dbProtocol: String,
+ dbHost: String,
+ dbPort: Int,
+ dbUsername: String,
+ dbPassword: String,
+ dbName: String)(implicit system: ActorSystem, val logging: Logging, jsonFormat: RootJsonFormat[DocumentAbstraction])
extends ArtifactStore[DocumentAbstraction]
with DefaultJsonProtocol {
- protected[core] implicit val executionContext = system.dispatcher
+ protected[core] implicit val executionContext = system.dispatcher
- private val client: CouchDbRestClient = new CouchDbRestClient(dbProtocol, dbHost, dbPort.toInt, dbUsername, dbPassword, dbName)
+ private val client: CouchDbRestClient =
+ new CouchDbRestClient(dbProtocol, dbHost, dbPort.toInt, dbUsername, dbPassword, dbName)
- override protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo] = {
- val asJson = d.toDocumentRecord
+ override protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo] = {
+ val asJson = d.toDocumentRecord
- val id: String = asJson.fields("_id").convertTo[String].trim
- val rev: Option[String] = asJson.fields.get("_rev").map(_.convertTo[String])
- require(!id.isEmpty, "document id must be defined")
+ val id: String = asJson.fields("_id").convertTo[String].trim
+ val rev: Option[String] = asJson.fields.get("_rev").map(_.convertTo[String])
+ require(!id.isEmpty, "document id must be defined")
- val docinfoStr = s"id: $id, rev: ${rev.getOrElse("null")}"
+ val docinfoStr = s"id: $id, rev: ${rev.getOrElse("null")}"
- val start = transid.started(this, LoggingMarkers.DATABASE_SAVE, s"[PUT] '$dbName' saving document: '${docinfoStr}'")
+ val start = transid.started(this, LoggingMarkers.DATABASE_SAVE, s"[PUT] '$dbName' saving document: '${docinfoStr}'")
- val request: CouchDbRestClient => Future[Either[StatusCode, JsObject]] = rev match {
- case Some(r) => client => client.putDoc(id, r, asJson)
- case None => client => client.putDoc(id, asJson)
- }
-
- val f = request(client).map { e =>
- e match {
- case Right(response) =>
- transid.finished(this, start, s"[PUT] '$dbName' completed document: '${docinfoStr}', response: '$response'")
- val id = response.fields("id").convertTo[String]
- val rev = response.fields("rev").convertTo[String]
- DocInfo ! (id, rev)
-
- case Left(StatusCodes.Conflict) =>
- transid.finished(this, start, s"[PUT] '$dbName', document: '${docinfoStr}'; conflict.")
- // For compatibility.
- throw DocumentConflictException("conflict on 'put'")
-
- case Left(code) =>
- transid.failed(this, start, s"[PUT] '$dbName' failed to put document: '${docinfoStr}'; http status: '${code}'", ErrorLevel)
- throw new Exception("Unexpected http response code: " + code)
- }
- }
-
- reportFailure(f, failure => transid.failed(this, start, s"[PUT] '$dbName' internal error, failure: '${failure.getMessage}'", ErrorLevel))
+ val request: CouchDbRestClient => Future[Either[StatusCode, JsObject]] = rev match {
+ case Some(r) =>
+ client =>
+ client.putDoc(id, r, asJson)
+ case None =>
+ client =>
+ client.putDoc(id, asJson)
}
- override protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {
- require(doc != null && doc.rev.asString != null, "doc revision required for delete")
-
- val start = transid.started(this, LoggingMarkers.DATABASE_DELETE, s"[DEL] '$dbName' deleting document: '$doc'")
-
- val f = client.deleteDoc(doc.id.id, doc.rev.rev).map { e =>
- e match {
- case Right(response) =>
- transid.finished(this, start, s"[DEL] '$dbName' completed document: '$doc', response: $response")
- response.fields("ok").convertTo[Boolean]
-
- case Left(StatusCodes.NotFound) =>
- transid.finished(this, start, s"[DEL] '$dbName', document: '${doc}'; not found.")
- // for compatibility
- throw NoDocumentException("not found on 'delete'")
-
- case Left(StatusCodes.Conflict) =>
- transid.finished(this, start, s"[DEL] '$dbName', document: '${doc}'; conflict.")
- throw DocumentConflictException("conflict on 'delete'")
-
- case Left(code) =>
- transid.failed(this, start, s"[DEL] '$dbName' failed to delete document: '${doc}'; http status: '${code}'", ErrorLevel)
- throw new Exception("Unexpected http response code: " + code)
- }
- }
-
- reportFailure(f, failure => transid.failed(this, start, s"[DEL] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'", ErrorLevel))
+ val f = request(client).map { e =>
+ e match {
+ case Right(response) =>
+ transid.finished(this, start, s"[PUT] '$dbName' completed document: '${docinfoStr}', response: '$response'")
+ val id = response.fields("id").convertTo[String]
+ val rev = response.fields("rev").convertTo[String]
+ DocInfo ! (id, rev)
+
+ case Left(StatusCodes.Conflict) =>
+ transid.finished(this, start, s"[PUT] '$dbName', document: '${docinfoStr}'; conflict.")
+ // For compatibility.
+ throw DocumentConflictException("conflict on 'put'")
+
+ case Left(code) =>
+ transid.failed(
+ this,
+ start,
+ s"[PUT] '$dbName' failed to put document: '${docinfoStr}'; http status: '${code}'",
+ ErrorLevel)
+ throw new Exception("Unexpected http response code: " + code)
+ }
}
- override protected[database] def get[A <: DocumentAbstraction](doc: DocInfo)(
- implicit transid: TransactionId,
- ma: Manifest[A]): Future[A] = {
-
- val start = transid.started(this, LoggingMarkers.DATABASE_GET, s"[GET] '$dbName' finding document: '$doc'")
-
- require(doc != null, "doc undefined")
- val request: CouchDbRestClient => Future[Either[StatusCode, JsObject]] = if (doc.rev.rev != null) {
- client => client.getDoc(doc.id.id, doc.rev.rev)
- } else {
- client => client.getDoc(doc.id.id)
- }
-
- val f = request(client).map { e =>
- e match {
- case Right(response) =>
- transid.finished(this, start, s"[GET] '$dbName' completed: found document '$doc'")
- val asFormat = jsonFormat.read(response)
- if (asFormat.getClass != ma.runtimeClass) {
- throw DocumentTypeMismatchException(s"document type ${asFormat.getClass} did not match expected type ${ma.runtimeClass}.")
- }
-
- val deserialized = asFormat.asInstanceOf[A]
-
- val responseRev = response.fields("_rev").convertTo[String]
- assert(doc.rev.rev == null || doc.rev.rev == responseRev, "Returned revision should match original argument")
- // FIXME remove mutability from appropriate classes now that it is no longer required by GSON.
- deserialized.asInstanceOf[WhiskDocument].revision(DocRevision(responseRev))
-
- deserialized
-
- case Left(StatusCodes.NotFound) =>
- transid.finished(this, start, s"[GET] '$dbName', document: '${doc}'; not found.")
- // for compatibility
- throw NoDocumentException("not found on 'get'")
-
- case Left(code) =>
- transid.finished(this, start, s"[GET] '$dbName' failed to get document: '${doc}'; http status: '${code}'")
- throw new Exception("Unexpected http response code: " + code)
- }
- } recoverWith {
- case e: DeserializationException => throw DocumentUnreadable(Messages.corruptedEntity)
- }
-
- reportFailure(f, failure => transid.failed(this, start, s"[GET] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'", ErrorLevel))
+ reportFailure(
+ f,
+ failure =>
+ transid.failed(this, start, s"[PUT] '$dbName' internal error, failure: '${failure.getMessage}'", ErrorLevel))
+ }
+
+ override protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {
+ require(doc != null && doc.rev.asString != null, "doc revision required for delete")
+
+ val start = transid.started(this, LoggingMarkers.DATABASE_DELETE, s"[DEL] '$dbName' deleting document: '$doc'")
+
+ val f = client.deleteDoc(doc.id.id, doc.rev.rev).map { e =>
+ e match {
+ case Right(response) =>
+ transid.finished(this, start, s"[DEL] '$dbName' completed document: '$doc', response: $response")
+ response.fields("ok").convertTo[Boolean]
+
+ case Left(StatusCodes.NotFound) =>
+ transid.finished(this, start, s"[DEL] '$dbName', document: '${doc}'; not found.")
+ // for compatibility
+ throw NoDocumentException("not found on 'delete'")
+
+ case Left(StatusCodes.Conflict) =>
+ transid.finished(this, start, s"[DEL] '$dbName', document: '${doc}'; conflict.")
+ throw DocumentConflictException("conflict on 'delete'")
+
+ case Left(code) =>
+ transid.failed(
+ this,
+ start,
+ s"[DEL] '$dbName' failed to delete document: '${doc}'; http status: '${code}'",
+ ErrorLevel)
+ throw new Exception("Unexpected http response code: " + code)
+ }
}
- override protected[core] def query(table: String, startKey: List[Any], endKey: List[Any], skip: Int, limit: Int, includeDocs: Boolean, descending: Boolean, reduce: Boolean, stale: StaleParameter)(
- implicit transid: TransactionId): Future[List[JsObject]] = {
-
- require(!(reduce && includeDocs), "reduce and includeDocs cannot both be true")
-
- // Apparently you have to do that in addition to setting "descending"
- val (realStartKey, realEndKey) = if (descending) {
- (endKey, startKey)
- } else {
- (startKey, endKey)
- }
-
- val parts = table.split("/")
-
- val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s"[QUERY] '$dbName' searching '$table")
-
- val f = for (
- eitherResponse <- client.executeView(parts(0), parts(1))(
- startKey = realStartKey,
- endKey = realEndKey,
- skip = Some(skip),
- limit = Some(limit),
- stale = stale,
- includeDocs = includeDocs,
- descending = descending,
- reduce = reduce)
- ) yield eitherResponse match {
- case Right(response) =>
- val rows = response.fields("rows").convertTo[List[JsObject]]
-
- val out = if (reduce && !rows.isEmpty) {
- assert(rows.length == 1, s"result of reduced view contains more than one value: '$rows'")
- rows.head.fields("value").convertTo[List[JsObject]]
- } else if (reduce) {
- List(JsObject())
- } else {
- rows
- }
-
- transid.finished(this, start, s"[QUERY] '$dbName' completed: matched ${out.size}")
- out
-
- case Left(code) =>
- transid.failed(this, start, s"Unexpected http response code: $code", ErrorLevel)
- throw new Exception("Unexpected http response code: " + code)
- }
-
- reportFailure(f, failure => transid.failed(this, start, s"[QUERY] '$dbName' internal error, failure: '${failure.getMessage}'", ErrorLevel))
+ reportFailure(
+ f,
+ failure =>
+ transid.failed(
+ this,
+ start,
+ s"[DEL] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'",
+ ErrorLevel))
+ }
+
+ override protected[database] def get[A <: DocumentAbstraction](doc: DocInfo)(implicit transid: TransactionId,
+ ma: Manifest[A]): Future[A] = {
+
+ val start = transid.started(this, LoggingMarkers.DATABASE_GET, s"[GET] '$dbName' finding document: '$doc'")
+
+ require(doc != null, "doc undefined")
+ val request: CouchDbRestClient => Future[Either[StatusCode, JsObject]] = if (doc.rev.rev != null) { client =>
+ client.getDoc(doc.id.id, doc.rev.rev)
+ } else { client =>
+ client.getDoc(doc.id.id)
}
- override protected[core] def attach(doc: DocInfo, name: String, contentType: ContentType, docStream: Source[ByteString, _])(
- implicit transid: TransactionId): Future[DocInfo] = {
-
- val start = transid.started(this, LoggingMarkers.DATABASE_ATT_SAVE, s"[ATT_PUT] '$dbName' uploading attachment '$name' of document '$doc'")
-
- require(doc != null, "doc undefined")
- require(doc.rev.rev != null, "doc revision must be specified")
-
- val f = client.putAttachment(doc.id.id, doc.rev.rev, name, contentType, docStream).map { e =>
- e match {
- case Right(response) =>
- transid.finished(this, start, s"[ATT_PUT] '$dbName' completed uploading attachment '$name' of document '$doc'")
- val id = response.fields("id").convertTo[String]
- val rev = response.fields("rev").convertTo[String]
- DocInfo ! (id, rev)
-
- case Left(StatusCodes.NotFound) =>
- transid.finished(this, start, s"[ATT_PUT] '$dbName' uploading attachment '$name' of document '$doc'; not found")
- throw NoDocumentException("Not found on 'readAttachment'.")
-
- case Left(code) =>
- transid.failed(this, start, s"[ATT_PUT] '$dbName' failed to upload attachment '$name' of document '$doc'; http status '$code'")
- throw new Exception("Unexpected http response code: " + code)
- }
- }
-
- reportFailure(f, failure => transid.failed(this, start, s"[ATT_PUT] '$dbName' internal error, name: '$name', doc: '$doc', failure: '${failure.getMessage}'", ErrorLevel))
+ val f = request(client).map { e =>
+ e match {
+ case Right(response) =>
+ transid.finished(this, start, s"[GET] '$dbName' completed: found document '$doc'")
+ val asFormat = jsonFormat.read(response)
+ if (asFormat.getClass != ma.runtimeClass) {
+ throw DocumentTypeMismatchException(
+ s"document type ${asFormat.getClass} did not match expected type ${ma.runtimeClass}.")
+ }
+
+ val deserialized = asFormat.asInstanceOf[A]
+
+ val responseRev = response.fields("_rev").convertTo[String]
+ assert(doc.rev.rev == null || doc.rev.rev == responseRev, "Returned revision should match original argument")
+ // FIXME remove mutability from appropriate classes now that it is no longer required by GSON.
+ deserialized.asInstanceOf[WhiskDocument].revision(DocRevision(responseRev))
+
+ deserialized
+
+ case Left(StatusCodes.NotFound) =>
+ transid.finished(this, start, s"[GET] '$dbName', document: '${doc}'; not found.")
+ // for compatibility
+ throw NoDocumentException("not found on 'get'")
+
+ case Left(code) =>
+ transid.finished(this, start, s"[GET] '$dbName' failed to get document: '${doc}'; http status: '${code}'")
+ throw new Exception("Unexpected http response code: " + code)
+ }
+ } recoverWith {
+ case e: DeserializationException => throw DocumentUnreadable(Messages.corruptedEntity)
}
- override protected[core] def readAttachment[T](doc: DocInfo, name: String, sink: Sink[ByteString, Future[T]])(
- implicit transid: TransactionId): Future[(ContentType, T)] = {
-
- val start = transid.started(this, LoggingMarkers.DATABASE_ATT_GET, s"[ATT_GET] '$dbName' finding attachment '$name' of document '$doc'")
-
- require(doc != null, "doc undefined")
- require(doc.rev.rev != null, "doc revision must be specified")
+ reportFailure(
+ f,
+ failure =>
+ transid.failed(
+ this,
+ start,
+ s"[GET] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'",
+ ErrorLevel))
+ }
+
+ override protected[core] def query(table: String,
+ startKey: List[Any],
+ endKey: List[Any],
+ skip: Int,
+ limit: Int,
+ includeDocs: Boolean,
+ descending: Boolean,
+ reduce: Boolean,
+ stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]] = {
+
+ require(!(reduce && includeDocs), "reduce and includeDocs cannot both be true")
+
+ // Apparently you have to do that in addition to setting "descending"
+ val (realStartKey, realEndKey) = if (descending) {
+ (endKey, startKey)
+ } else {
+ (startKey, endKey)
+ }
- val f = client.getAttachment[T](doc.id.id, doc.rev.rev, name, sink)
- val g = f.map { e =>
- e match {
- case Right((contentType, result)) =>
- transid.finished(this, start, s"[ATT_GET] '$dbName' completed: found attachment '$name' of document '$doc'")
- (contentType, result)
+ val parts = table.split("/")
+
+ val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s"[QUERY] '$dbName' searching '$table")
+
+ val f = for (eitherResponse <- client.executeView(parts(0), parts(1))(
+ startKey = realStartKey,
+ endKey = realEndKey,
+ skip = Some(skip),
+ limit = Some(limit),
+ stale = stale,
+ includeDocs = includeDocs,
+ descending = descending,
+ reduce = reduce))
+ yield
+ eitherResponse match {
+ case Right(response) =>
+ val rows = response.fields("rows").convertTo[List[JsObject]]
+
+ val out = if (reduce && !rows.isEmpty) {
+ assert(rows.length == 1, s"result of reduced view contains more than one value: '$rows'")
+ rows.head.fields("value").convertTo[List[JsObject]]
+ } else if (reduce) {
+ List(JsObject())
+ } else {
+ rows
+ }
- case Left(StatusCodes.NotFound) =>
- transid.finished(this, start, s"[ATT_GET] '$dbName', retrieving attachment '$name' of document '$doc'; not found.")
- throw NoDocumentException("Not found on 'readAttachment'.")
+ transid.finished(this, start, s"[QUERY] '$dbName' completed: matched ${out.size}")
+ out
- case Left(code) =>
- transid.failed(this, start, s"[ATT_GET] '$dbName' failed to get attachment '$name' of document '$doc'; http status: '${code}'")
- throw new Exception("Unexpected http response code: " + code)
- }
+ case Left(code) =>
+ transid.failed(this, start, s"Unexpected http response code: $code", ErrorLevel)
+ throw new Exception("Unexpected http response code: " + code)
}
- reportFailure(g, failure => transid.failed(this, start, s"[ATT_GET] '$dbName' internal error, name: '$name', doc: '$doc', failure: '${failure.getMessage}'", ErrorLevel))
+ reportFailure(
+ f,
+ failure =>
+ transid.failed(this, start, s"[QUERY] '$dbName' internal error, failure: '${failure.getMessage}'", ErrorLevel))
+ }
+
+ override protected[core] def attach(
+ doc: DocInfo,
+ name: String,
+ contentType: ContentType,
+ docStream: Source[ByteString, _])(implicit transid: TransactionId): Future[DocInfo] = {
+
+ val start = transid.started(
+ this,
+ LoggingMarkers.DATABASE_ATT_SAVE,
+ s"[ATT_PUT] '$dbName' uploading attachment '$name' of document '$doc'")
+
+ require(doc != null, "doc undefined")
+ require(doc.rev.rev != null, "doc revision must be specified")
+
+ val f = client.putAttachment(doc.id.id, doc.rev.rev, name, contentType, docStream).map { e =>
+ e match {
+ case Right(response) =>
+ transid
+ .finished(this, start, s"[ATT_PUT] '$dbName' completed uploading attachment '$name' of document '$doc'")
+ val id = response.fields("id").convertTo[String]
+ val rev = response.fields("rev").convertTo[String]
+ DocInfo ! (id, rev)
+
+ case Left(StatusCodes.NotFound) =>
+ transid
+ .finished(this, start, s"[ATT_PUT] '$dbName' uploading attachment '$name' of document '$doc'; not found")
+ throw NoDocumentException("Not found on 'readAttachment'.")
+
+ case Left(code) =>
+ transid.failed(
+ this,
+ start,
+ s"[ATT_PUT] '$dbName' failed to upload attachment '$name' of document '$doc'; http status '$code'")
+ throw new Exception("Unexpected http response code: " + code)
+ }
}
- override def shutdown(): Unit = {
- Await.ready(client.shutdown(), 1.minute)
+ reportFailure(
+ f,
+ failure =>
+ transid.failed(
+ this,
+ start,
+ s"[ATT_PUT] '$dbName' internal error, name: '$name', doc: '$doc', failure: '${failure.getMessage}'",
+ ErrorLevel))
+ }
+
+ override protected[core] def readAttachment[T](doc: DocInfo, name: String, sink: Sink[ByteString, Future[T]])(
+ implicit transid: TransactionId): Future[(ContentType, T)] = {
+
+ val start = transid.started(
+ this,
+ LoggingMarkers.DATABASE_ATT_GET,
+ s"[ATT_GET] '$dbName' finding attachment '$name' of document '$doc'")
+
+ require(doc != null, "doc undefined")
+ require(doc.rev.rev != null, "doc revision must be specified")
+
+ val f = client.getAttachment[T](doc.id.id, doc.rev.rev, name, sink)
+ val g = f.map { e =>
+ e match {
+ case Right((contentType, result)) =>
+ transid.finished(this, start, s"[ATT_GET] '$dbName' completed: found attachment '$name' of document '$doc'")
+ (contentType, result)
+
+ case Left(StatusCodes.NotFound) =>
+ transid.finished(
+ this,
+ start,
+ s"[ATT_GET] '$dbName', retrieving attachment '$name' of document '$doc'; not found.")
+ throw NoDocumentException("Not found on 'readAttachment'.")
+
+ case Left(code) =>
+ transid.failed(
+ this,
+ start,
+ s"[ATT_GET] '$dbName' failed to get attachment '$name' of document '$doc'; http status: '${code}'")
+ throw new Exception("Unexpected http response code: " + code)
+ }
}
- private def reportFailure[T, U](f: Future[T], onFailure: Throwable => U): Future[T] = {
- f.onFailure({
- case _: ArtifactStoreException => // These failures are intentional and shouldn't trigger the catcher.
- case x => onFailure(x)
- })
- f
- }
+ reportFailure(
+ g,
+ failure =>
+ transid.failed(
+ this,
+ start,
+ s"[ATT_GET] '$dbName' internal error, name: '$name', doc: '$doc', failure: '${failure.getMessage}'",
+ ErrorLevel))
+ }
+
+ override def shutdown(): Unit = {
+ Await.ready(client.shutdown(), 1.minute)
+ }
+
+ private def reportFailure[T, U](f: Future[T], onFailure: Throwable => U): Future[T] = {
+ f.onFailure({
+ case _: ArtifactStoreException => // These failures are intentional and shouldn't trigger the catcher.
+ case x => onFailure(x)
+ })
+ f
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala b/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
index 9a57035..e3713d8 100644
--- a/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
+++ b/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
@@ -24,14 +24,25 @@ import whisk.core.WhiskConfig
object CouchDbStoreProvider extends ArtifactStoreProvider {
- def makeStore[D <: DocumentSerializer](config: WhiskConfig, name: WhiskConfig => String)(
- implicit jsonFormat: RootJsonFormat[D],
- actorSystem: ActorSystem,
- logging: Logging): ArtifactStore[D] = {
- require(config != null && config.isValid, "config is undefined or not valid")
- require(config.dbProvider == "Cloudant" || config.dbProvider == "CouchDB", "Unsupported db.provider: " + config.dbProvider)
- assume(Set(config.dbProtocol, config.dbHost, config.dbPort, config.dbUsername, config.dbPassword, name(config)).forall(_.nonEmpty), "At least one expected property is missing")
+ def makeStore[D <: DocumentSerializer](config: WhiskConfig, name: WhiskConfig => String)(
+ implicit jsonFormat: RootJsonFormat[D],
+ actorSystem: ActorSystem,
+ logging: Logging): ArtifactStore[D] = {
+ require(config != null && config.isValid, "config is undefined or not valid")
+ require(
+ config.dbProvider == "Cloudant" || config.dbProvider == "CouchDB",
+ "Unsupported db.provider: " + config.dbProvider)
+ assume(
+ Set(config.dbProtocol, config.dbHost, config.dbPort, config.dbUsername, config.dbPassword, name(config))
+ .forall(_.nonEmpty),
+ "At least one expected property is missing")
- new CouchDbRestStore[D](config.dbProtocol, config.dbHost, config.dbPort.toInt, config.dbUsername, config.dbPassword, name(config))
- }
+ new CouchDbRestStore[D](
+ config.dbProtocol,
+ config.dbHost,
+ config.dbPort.toInt,
+ config.dbUsername,
+ config.dbPassword,
+ name(config))
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/database/DocumentFactory.scala b/common/scala/src/main/scala/whisk/core/database/DocumentFactory.scala
index 451bae9..6aa9515 100644
--- a/common/scala/src/main/scala/whisk/core/database/DocumentFactory.scala
+++ b/common/scala/src/main/scala/whisk/core/database/DocumentFactory.scala
@@ -46,34 +46,36 @@ import whisk.core.entity.DocRevision
* the datastore is declared as a var for that purpose.
*/
trait Document {
- /** The document id, this is the primary key for the document and must be unique. */
- protected var _id: String = null
- /** The document revision as determined by the datastore; an opaque value. */
- protected[database] var _rev: String = null
- /** Gets the document id and revision as an instance of DocInfo. */
- protected[database] def docinfo: DocInfo
+ /** The document id, this is the primary key for the document and must be unique. */
+ protected var _id: String = null
- /**
- * Checks if the document has a valid revision set, in which case
- * this is an update operation.
- *
- * @return true iff document has a valid revision
- */
- protected[database] final def update: Boolean = _rev != null
+ /** The document revision as determined by the datastore; an opaque value. */
+ protected[database] var _rev: String = null
- /**
- * Confirms the document has a valid id set.
- *
- * @return true iff document has a valid id
- * @throws IllegalArgumentException iff document does not have a valid id
- */
- @throws[IllegalArgumentException]
- protected[database] final def confirmId: Boolean = {
- require(_id != null, "document id undefined")
- require(_id.trim.nonEmpty, "document id undefined")
- true
- }
+ /** Gets the document id and revision as an instance of DocInfo. */
+ protected[database] def docinfo: DocInfo
+
+ /**
+ * Checks if the document has a valid revision set, in which case
+ * this is an update operation.
+ *
+ * @return true iff document has a valid revision
+ */
+ protected[database] final def update: Boolean = _rev != null
+
+ /**
+ * Confirms the document has a valid id set.
+ *
+ * @return true iff document has a valid id
+ * @throws IllegalArgumentException iff document does not have a valid id
+ */
+ @throws[IllegalArgumentException]
+ protected[database] final def confirmId: Boolean = {
+ require(_id != null, "document id undefined")
+ require(_id.trim.nonEmpty, "document id undefined")
+ true
+ }
}
/**
@@ -82,20 +84,21 @@ trait Document {
* need to update the revision on a document.
*/
protected[core] trait DocumentRevisionProvider {
- /**
- * Sets the revision number when a document is deserialized from datastore. The
- * _rev is an opaque value, needed to update the record in the datastore. It is
- * not part of the core properties of this class. It is not required when saving
- * a new instance of this type to the datastore.
- */
- protected[core] final def revision[W](r: DocRevision): W = {
- _rev = r
- this.asInstanceOf[W]
- }
- protected[core] def rev = _rev
+ /**
+ * Sets the revision number when a document is deserialized from datastore. The
+ * _rev is an opaque value, needed to update the record in the datastore. It is
+ * not part of the core properties of this class. It is not required when saving
+ * a new instance of this type to the datastore.
+ */
+ protected[core] final def revision[W](r: DocRevision): W = {
+ _rev = r
+ this.asInstanceOf[W]
+ }
+
+ protected[core] def rev = _rev
- private var _rev: DocRevision = DocRevision.empty
+ private var _rev: DocRevision = DocRevision.empty
}
/**
@@ -103,12 +106,13 @@ protected[core] trait DocumentRevisionProvider {
* the datastore, where the document id is a generated unique identifier.
*/
trait DocumentSerializer {
- /**
- * A JSON view including the document metadata, for writing to the datastore.
- *
- * @return JsObject
- */
- def toDocumentRecord: JsObject
+
+ /**
+ * A JSON view including the document metadata, for writing to the datastore.
+ *
+ * @return JsObject
+ */
+ def toDocumentRecord: JsObject
}
/**
@@ -120,137 +124,150 @@ trait DocumentSerializer {
* may be used for multiple types (because the types are stored in the same database for example).
*/
trait DocumentFactory[W] extends MultipleReadersSingleWriterCache[W, DocInfo] {
- /**
- * Puts a record of type W in the datastore.
- *
- * The type parameters for the database are bounded from below to allow gets from a database that
- * contains several different but related types (for example entities are stored in the same database
- * and share common super types EntityRecord and WhiskEntity.
- *
- * @param db the datastore client to fetch entity from
- * @param doc the entity to store
- * @param transid the transaction id for logging
- * @param notifier an optional callback when cache changes
- * @return Future[DocInfo] with completion to DocInfo containing the save document id and revision
- */
- def put[Wsuper >: W](db: ArtifactStore[Wsuper], doc: W)(
- implicit transid: TransactionId, notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
- Try {
- require(db != null, "db undefined")
- require(doc != null, "doc undefined")
- } map { _ =>
- implicit val logger = db.logging
- implicit val ec = db.executionContext
- val key = CacheKey(doc)
+ /**
+ * Puts a record of type W in the datastore.
+ *
+ * The type parameters for the database are bounded from below to allow gets from a database that
+ * contains several different but related types (for example entities are stored in the same database
+ * and share common super types EntityRecord and WhiskEntity.
+ *
+ * @param db the datastore client to fetch entity from
+ * @param doc the entity to store
+ * @param transid the transaction id for logging
+ * @param notifier an optional callback when cache changes
+ * @return Future[DocInfo] with completion to DocInfo containing the save document id and revision
+ */
+ def put[Wsuper >: W](db: ArtifactStore[Wsuper], doc: W)(
+ implicit transid: TransactionId,
+ notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
+ Try {
+ require(db != null, "db undefined")
+ require(doc != null, "doc undefined")
+ } map { _ =>
+ implicit val logger = db.logging
+ implicit val ec = db.executionContext
- cacheUpdate(doc, key, db.put(doc) map { docinfo =>
- doc match {
- // if doc has a revision id, update it with new version
- case w: DocumentRevisionProvider => w.revision[W](docinfo.rev)
- }
- docinfo
- })
+ val key = CacheKey(doc)
- } match {
- case Success(f) => f
- case Failure(t) => Future.failed(t)
+ cacheUpdate(doc, key, db.put(doc) map { docinfo =>
+ doc match {
+ // if doc has a revision id, update it with new version
+ case w: DocumentRevisionProvider => w.revision[W](docinfo.rev)
}
+ docinfo
+ })
+
+ } match {
+ case Success(f) => f
+ case Failure(t) => Future.failed(t)
}
+ }
- def attach[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocInfo, attachmentName: String, contentType: ContentType, bytes: InputStream)(
- implicit transid: TransactionId, notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
+ def attach[Wsuper >: W](
+ db: ArtifactStore[Wsuper],
+ doc: DocInfo,
+ attachmentName: String,
+ contentType: ContentType,
+ bytes: InputStream)(implicit transid: TransactionId, notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
- Try {
- require(db != null, "db undefined")
- require(doc != null, "doc undefined")
- } map { _ =>
- implicit val logger = db.logging
- implicit val ec = db.executionContext
+ Try {
+ require(db != null, "db undefined")
+ require(doc != null, "doc undefined")
+ } map { _ =>
+ implicit val logger = db.logging
+ implicit val ec = db.executionContext
- val key = CacheKey(doc.id.asDocInfo)
- // invalidate the key because attachments update the revision;
- // do not cache the new attachment (controller does not need it)
- cacheInvalidate(key, {
- val src = StreamConverters.fromInputStream(() => bytes)
- db.attach(doc, attachmentName, contentType, src)
- })
- } match {
- case Success(f) => f
- case Failure(t) => Future.failed(t)
- }
+ val key = CacheKey(doc.id.asDocInfo)
+ // invalidate the key because attachments update the revision;
+ // do not cache the new attachment (controller does not need it)
+ cacheInvalidate(key, {
+ val src = StreamConverters.fromInputStream(() => bytes)
+ db.attach(doc, attachmentName, contentType, src)
+ })
+ } match {
+ case Success(f) => f
+ case Failure(t) => Future.failed(t)
}
+ }
- def del[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocInfo)(
- implicit transid: TransactionId, notifier: Option[CacheChangeNotification]): Future[Boolean] = {
- Try {
- require(db != null, "db undefined")
- require(doc != null, "doc undefined")
- } map { _ =>
- implicit val logger = db.logging
- implicit val ec = db.executionContext
+ def del[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocInfo)(
+ implicit transid: TransactionId,
+ notifier: Option[CacheChangeNotification]): Future[Boolean] = {
+ Try {
+ require(db != null, "db undefined")
+ require(doc != null, "doc undefined")
+ } map { _ =>
+ implicit val logger = db.logging
+ implicit val ec = db.executionContext
- val key = CacheKey(doc.id.asDocInfo)
- cacheInvalidate(key, db.del(doc))
- } match {
- case Success(f) => f
- case Failure(t) => Future.failed(t)
- }
+ val key = CacheKey(doc.id.asDocInfo)
+ cacheInvalidate(key, db.del(doc))
+ } match {
+ case Success(f) => f
+ case Failure(t) => Future.failed(t)
}
+ }
- /**
- * Fetches a raw record of type R from the datastore by its id (and revision if given)
- * and converts it to Success(W) or Failure(Throwable) if there is an error fetching
- * the record or deserializing it.
- *
- * The type parameters for the database are bounded from below to allow gets from a database that
- * contains several different but related types (for example entities are stored in the same database
- * and share common super types EntityRecord and WhiskEntity.
- *
- * @param db the datastore client to fetch entity from
- * @param doc the entity document information (must contain a valid id)
- * @param rev the document revision (optional)
- * @param fromCache will only query cache if true (defaults to collection settings)
- * @param transid the transaction id for logging
- * @param mw a manifest for W (hint to compiler to preserve type R for runtime)
- * @return Future[W] with completion to Success(W), or Failure(Throwable) if the raw record cannot be converted into W
- */
- def get[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocId, rev: DocRevision = DocRevision.empty, fromCache: Boolean = cacheEnabled)(
- implicit transid: TransactionId, mw: Manifest[W]): Future[W] = {
- Try {
- require(db != null, "db undefined")
- } map {
- implicit val logger = db.logging
- implicit val ec = db.executionContext
- val key = doc.asDocInfo(rev)
- _ => cacheLookup(CacheKey(key), db.get[W](key), fromCache)
- } match {
- case Success(f) => f
- case Failure(t) => Future.failed(t)
- }
+ /**
+ * Fetches a raw record of type R from the datastore by its id (and revision if given)
+ * and converts it to Success(W) or Failure(Throwable) if there is an error fetching
+ * the record or deserializing it.
+ *
+ * The type parameters for the database are bounded from below to allow gets from a database that
+ * contains several different but related types (for example entities are stored in the same database
+ * and share common super types EntityRecord and WhiskEntity.
+ *
+ * @param db the datastore client to fetch entity from
+ * @param doc the entity document information (must contain a valid id)
+ * @param rev the document revision (optional)
+ * @param fromCache will only query cache if true (defaults to collection settings)
+ * @param transid the transaction id for logging
+ * @param mw a manifest for W (hint to compiler to preserve type R for runtime)
+ * @return Future[W] with completion to Success(W), or Failure(Throwable) if the raw record cannot be converted into W
+ */
+ def get[Wsuper >: W](
+ db: ArtifactStore[Wsuper],
+ doc: DocId,
+ rev: DocRevision = DocRevision.empty,
+ fromCache: Boolean = cacheEnabled)(implicit transid: TransactionId, mw: Manifest[W]): Future[W] = {
+ Try {
+ require(db != null, "db undefined")
+ } map {
+ implicit val logger = db.logging
+ implicit val ec = db.executionContext
+ val key = doc.asDocInfo(rev)
+ _ =>
+ cacheLookup(CacheKey(key), db.get[W](key), fromCache)
+ } match {
+ case Success(f) => f
+ case Failure(t) => Future.failed(t)
}
+ }
- def getAttachment[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocInfo, attachmentName: String, outputStream: OutputStream)(
- implicit transid: TransactionId): Future[Unit] = {
+ def getAttachment[Wsuper >: W](db: ArtifactStore[Wsuper],
+ doc: DocInfo,
+ attachmentName: String,
+ outputStream: OutputStream)(implicit transid: TransactionId): Future[Unit] = {
- implicit val ec = db.executionContext
+ implicit val ec = db.executionContext
- Try {
- require(db != null, "db defined")
- require(doc != null, "doc undefined")
- } map { _ =>
- val sink = StreamConverters.fromOutputStream(() => outputStream)
- db.readAttachment[IOResult](doc, attachmentName, sink).map {
- case (_, r) =>
- if (!r.wasSuccessful) {
- // FIXME...
- // Figure out whether OutputStreams are even a decent model.
- }
- ()
- }
- } match {
- case Success(f) => f
- case Failure(t) => Future.failed(t)
- }
+ Try {
+ require(db != null, "db defined")
+ require(doc != null, "doc undefined")
+ } map { _ =>
+ val sink = StreamConverters.fromOutputStream(() => outputStream)
+ db.readAttachment[IOResult](doc, attachmentName, sink).map {
+ case (_, r) =>
+ if (!r.wasSuccessful) {
+ // FIXME...
+ // Figure out whether OutputStreams are even a decent model.
+ }
+ ()
+ }
+ } match {
+ case Success(f) => f
+ case Failure(t) => Future.failed(t)
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/database/MultipleReadersSingleWriterCache.scala b/common/scala/src/main/scala/whisk/core/database/MultipleReadersSingleWriterCache.scala
index f767c60..4d45ea0 100644
--- a/common/scala/src/main/scala/whisk/core/database/MultipleReadersSingleWriterCache.scala
+++ b/common/scala/src/main/scala/whisk/core/database/MultipleReadersSingleWriterCache.scala
@@ -77,362 +77,380 @@ import whisk.core.entity.CacheKey
*
*/
private object MultipleReadersSingleWriterCache {
- /** Each entry has a state, as explained in the class comment above. */
- object State extends Enumeration {
- type State = Value
- val ReadInProgress, WriteInProgress, InvalidateInProgress, InvalidateWhenDone, Cached = Value
- }
- import State._
+ /** Each entry has a state, as explained in the class comment above. */
+ object State extends Enumeration {
+ type State = Value
+ val ReadInProgress, WriteInProgress, InvalidateInProgress, InvalidateWhenDone, Cached = Value
+ }
+
+ import State._
- /** Failure modes, which will only occur if there is a bug in this implementation */
- case class ConcurrentOperationUnderRead(actualState: State) extends Exception(s"Cache read started, but completion raced with a concurrent operation: $actualState.")
- case class ConcurrentOperationUnderUpdate(actualState: State) extends Exception(s"Cache update started, but completion raced with a concurrent operation: $actualState.")
- case class StaleRead(actualState: State) extends Exception(s"Attempted read of invalid entry due to $actualState.")
+ /** Failure modes, which will only occur if there is a bug in this implementation */
+ case class ConcurrentOperationUnderRead(actualState: State)
+ extends Exception(s"Cache read started, but completion raced with a concurrent operation: $actualState.")
+ case class ConcurrentOperationUnderUpdate(actualState: State)
+ extends Exception(s"Cache update started, but completion raced with a concurrent operation: $actualState.")
+ case class StaleRead(actualState: State) extends Exception(s"Attempted read of invalid entry due to $actualState.")
}
trait CacheChangeNotification extends (CacheKey => Future[Unit])
trait MultipleReadersSingleWriterCache[W, Winfo] {
- import MultipleReadersSingleWriterCache._
- import MultipleReadersSingleWriterCache.State._
+ import MultipleReadersSingleWriterCache._
+ import MultipleReadersSingleWriterCache.State._
- /** Subclasses: Toggle this to enable/disable caching for your entity type. */
- protected val cacheEnabled = true
+ /** Subclasses: Toggle this to enable/disable caching for your entity type. */
+ protected val cacheEnabled = true
- private object Entry {
- def apply(transid: TransactionId, state: State, value: Option[Future[W]]): Entry = {
- new Entry(transid, new AtomicReference(state), value)
- }
+ private object Entry {
+ def apply(transid: TransactionId, state: State, value: Option[Future[W]]): Entry = {
+ new Entry(transid, new AtomicReference(state), value)
}
-
- /**
- * The entries in the cache will be a triple of (transid, State, Future[W]?).
- *
- * We need the transid in order to detect whether we have won the race to add an entry to the cache.
- */
- private class Entry(
- @volatile private var transid: TransactionId,
- val state: AtomicReference[State],
- @volatile private var value: Option[Future[W]]) {
-
- def invalidate(): Unit = {
- state.set(InvalidateInProgress)
- }
-
- def unpack(): Future[W] = {
- value getOrElse Future.failed(StaleRead(state.get))
- }
-
- def writeDone()(implicit logger: Logging): Boolean = {
- logger.debug(this, "write finished")(transid)
- trySet(WriteInProgress, Cached)
- }
-
- def readDone()(implicit logger: Logging): Boolean = {
- logger.debug(this, "read finished")(transid)
- trySet(ReadInProgress, Cached)
- }
-
- def trySet(expectedState: State, desiredState: State): Boolean = {
- state.compareAndSet(expectedState, desiredState)
- }
-
- def grabWriteLock(newTransid: TransactionId, expectedState: State, newValue: Future[W]): Boolean = synchronized {
- val swapped = trySet(expectedState, WriteInProgress)
- if (swapped) {
- value = Option(newValue)
- transid = newTransid
- }
- swapped
- }
-
- def grabInvalidationLock() = state.set(InvalidateInProgress)
-
- override def toString = s"tid ${transid.meta.id}, state ${state.get}"
+ }
+
+ /**
+ * The entries in the cache will be a triple of (transid, State, Future[W]?).
+ *
+ * We need the transid in order to detect whether we have won the race to add an entry to the cache.
+ */
+ private class Entry(@volatile private var transid: TransactionId,
+ val state: AtomicReference[State],
+ @volatile private var value: Option[Future[W]]) {
+
+ def invalidate(): Unit = {
+ state.set(InvalidateInProgress)
}
- /**
- * This method posts a delete to the backing store, and either directly invalidates the cache entry
- * or informs any outstanding transaction that it must invalidate the cache on completion.
- */
- protected def cacheInvalidate[R](key: CacheKey, invalidator: => Future[R])(
- implicit ec: ExecutionContext, transid: TransactionId, logger: Logging, notifier: Option[CacheChangeNotification]): Future[R] = {
- if (cacheEnabled) {
- logger.info(this, s"invalidating $key on delete")
-
- notifier.foreach(_(key))
-
- // try inserting our desired entry...
- val desiredEntry = Entry(transid, InvalidateInProgress, None)
- cache(key)(desiredEntry) flatMap { actualEntry =>
- // ... and see what we get back
- val currentState = actualEntry.state.get
-
- currentState match {
- case Cached =>
- // nobody owns the entry, forcefully grab ownership
- // note: if a new cache lookup is received while
- // the invalidator has not yet completed (and hence the actual entry
- // removed from the cache), such lookup operations will still be able
- // to return the value that is cached, and this is acceptable (under
- // the eventual consistency model) as long as such lookups do not
- // mutate the state of the cache to violate the invalidation that is
- // about to occur (this is eventually consistent and NOT sequentially
- // consistent since the cache lookup and the setting of the
- // InvalidateInProgress bit are not atomic
- invalidateEntryAfter(invalidator, key, actualEntry)
-
- case ReadInProgress | WriteInProgress =>
- if (actualEntry.trySet(currentState, InvalidateWhenDone)) {
- // then the pre-existing owner will take care of the invalidation
- invalidator
- } else {
- // the pre-existing reader or writer finished and so must
- // explicitly invalidate here
- invalidateEntryAfter(invalidator, key, actualEntry)
- }
-
- case InvalidateInProgress =>
- if (actualEntry == desiredEntry) {
- // we own the entry, so we are responsible for cleaning it up
- invalidateEntryAfter(invalidator, key, actualEntry)
- } else {
- // someone else requested an invalidation already
- invalidator
- }
-
- case InvalidateWhenDone =>
- // a pre-existing owner will take care of the invalidation
- invalidator
- }
- }
- } else invalidator // not caching
+ def unpack(): Future[W] = {
+ value getOrElse Future.failed(StaleRead(state.get))
}
- /**
- * This method may initiate a read from the backing store, and potentially stores the result in the cache.
- */
- protected def cacheLookup[Wsuper >: W](key: CacheKey, generator: => Future[W], fromCache: Boolean = cacheEnabled)(
- implicit ec: ExecutionContext, transid: TransactionId, logger: Logging): Future[W] = {
- if (fromCache) {
- val promise = Promise[W] // this promise completes with the generator value
-
- // try inserting our desired entry...
- val desiredEntry = Entry(transid, ReadInProgress, Some(promise.future))
- cache(key)(desiredEntry) flatMap { actualEntry =>
- // ... and see what we get back
-
- actualEntry.state.get match {
- case Cached =>
- logger.debug(this, "cached read")
- makeNoteOfCacheHit(key)
- actualEntry.unpack
-
- case ReadInProgress =>
- if (actualEntry == desiredEntry) {
- logger.debug(this, "read initiated");
- makeNoteOfCacheMiss(key)
- // updating the cache with the new value is done in the listener
- // and will complete unless an invalidation request or an intervening
- // write occur in the meantime
- listenForReadDone(key, actualEntry, generator, promise)
- actualEntry.unpack
- } else {
- logger.debug(this, "coalesced read")
- makeNoteOfCacheHit(key)
- actualEntry.unpack
- }
-
- case WriteInProgress | InvalidateInProgress =>
- logger.debug(this, "reading around an update in progress")
- makeNoteOfCacheMiss(key)
- generator
- }
- }
- } else generator // not caching
+ def writeDone()(implicit logger: Logging): Boolean = {
+ logger.debug(this, "write finished")(transid)
+ trySet(WriteInProgress, Cached)
}
- /**
- * This method posts an update to the backing store, and potentially stores the result in the cache.
- */
- protected def cacheUpdate(doc: W, key: CacheKey, generator: => Future[Winfo])(
- implicit ec: ExecutionContext, transid: TransactionId, logger: Logging, notifier: Option[CacheChangeNotification]): Future[Winfo] = {
- if (cacheEnabled) {
-
- notifier.foreach(_(key))
-
- // try inserting our desired entry...
- val desiredEntry = Entry(transid, WriteInProgress, Some(Future.successful(doc)))
- cache(key)(desiredEntry) flatMap { actualEntry =>
- // ... and see what we get back
-
- if (actualEntry == desiredEntry) {
- // then this transaction won the race to insert a new entry in the cache
- // and it is responsible for updating the cache entry...
- logger.info(this, s"write initiated on new cache entry")
- listenForWriteDone(key, actualEntry, generator)
- } else {
- // ... otherwise, some existing entry is in the way, so try to grab a write lock
- val currentState = actualEntry.state.get
- val allowedToAssumeCompletion = currentState == Cached || currentState == ReadInProgress
-
- if (allowedToAssumeCompletion && actualEntry.grabWriteLock(transid, currentState, desiredEntry.unpack)) {
- // this transaction is now responsible for updating the cache entry
- logger.info(this, s"write initiated on existing cache entry, invalidating $key, $actualEntry")
- listenForWriteDone(key, actualEntry, generator)
- } else {
- // there is a conflicting operation in progress on this key
- logger.info(this, s"write-around (i.e., not cached) under $currentState")
- invalidateEntryAfter(generator, key, actualEntry)
- }
- }
- }
- } else generator // not caching
+ def readDone()(implicit logger: Logging): Boolean = {
+ logger.debug(this, "read finished")(transid)
+ trySet(ReadInProgress, Cached)
}
- def cacheSize: Int = cache.size
-
- /**
- * This method removes an entry from the cache immediately. You can use this method
- * if you do not need to perform any updates on the backing store but only to the cache.
- */
- protected[database] def removeId(key: CacheKey)(implicit ec: ExecutionContext): Unit = cache.remove(key)
-
- /**
- * Log a cache hit
- *
- */
- private def makeNoteOfCacheHit(key: CacheKey)(implicit transid: TransactionId, logger: Logging) = {
- transid.mark(this, LoggingMarkers.DATABASE_CACHE_HIT, s"[GET] serving from cache: $key")(logger)
+ def trySet(expectedState: State, desiredState: State): Boolean = {
+ state.compareAndSet(expectedState, desiredState)
}
- /**
- * Log a cache miss
- *
- */
- private def makeNoteOfCacheMiss(key: CacheKey)(implicit transid: TransactionId, logger: Logging) = {
- transid.mark(this, LoggingMarkers.DATABASE_CACHE_MISS, s"[GET] serving from datastore: $key")(logger)
+ def grabWriteLock(newTransid: TransactionId, expectedState: State, newValue: Future[W]): Boolean = synchronized {
+ val swapped = trySet(expectedState, WriteInProgress)
+ if (swapped) {
+ value = Option(newValue)
+ transid = newTransid
+ }
+ swapped
}
- /**
- * We have initiated a read (in cacheLookup), now handle its completion:
- * 1. either cache the result if there is no intervening delete or update, or
- * 2. invalidate the cache because there was an intervening delete or update.
- */
- private def listenForReadDone(key: CacheKey, entry: Entry, generator: => Future[W], promise: Promise[W])(
- implicit ec: ExecutionContext, transid: TransactionId, logger: Logging): Unit = {
-
- generator onComplete {
- case Success(value) =>
- // if the datastore read was successful, then try to transition to the Cached state
- logger.debug(this, "read backend part done, now marking cache entry as done")
-
- // always complete the promise for the generator since the read listener is
- // only created when reading directly from the database (hence, must complete
- // promise with the generated value)
- promise success value
-
- // now update the cache line
- if (entry.readDone()) {
- // cache entry is still in ReadInProgress and successful transitioned to Cached
- // hence the new value is cached; nothing left to do
- } else {
- val cachedLineState = entry.state.get
-
- cachedLineState match {
- case WriteInProgress | Cached =>
- // do nothing: if there was a write in progress, the write has not yet
- // finished, but that operation has assumed ownership of the cache line
- // and will update it; otherwise the write has completed and the value
- // is now cached
- ()
- case _ =>
- // some transaction requested an invalidation so remove the key from the cache,
- // or there is an error in which case invalidate anyway, defensively, but log a message
- invalidateEntry(key, entry)
- if (cachedLineState != InvalidateWhenDone) {
- // this should not happen, but could if the callback on the generator
- // is delayed - invalidate the cache entry as a result
- val error = ConcurrentOperationUnderRead(cachedLineState)
- logger.warn(this, error.toString)
- }
- }
- }
-
- case Failure(t) =>
- // oops, the datastore read failed. invalidate the cache entry
- // note: that this might be a perfectly legitimate failure,
- // e.g. a lookup for a non-existant key; we need to pass the particular t through
- invalidateEntry(key, entry)
- promise.failure(t)
+ def grabInvalidationLock() = state.set(InvalidateInProgress)
+
+ override def toString = s"tid ${transid.meta.id}, state ${state.get}"
+ }
+
+ /**
+ * This method posts a delete to the backing store, and either directly invalidates the cache entry
+ * or informs any outstanding transaction that it must invalidate the cache on completion.
+ */
+ protected def cacheInvalidate[R](key: CacheKey, invalidator: => Future[R])(
+ implicit ec: ExecutionContext,
+ transid: TransactionId,
+ logger: Logging,
+ notifier: Option[CacheChangeNotification]): Future[R] = {
+ if (cacheEnabled) {
+ logger.info(this, s"invalidating $key on delete")
+
+ notifier.foreach(_(key))
+
+ // try inserting our desired entry...
+ val desiredEntry = Entry(transid, InvalidateInProgress, None)
+ cache(key)(desiredEntry) flatMap { actualEntry =>
+ // ... and see what we get back
+ val currentState = actualEntry.state.get
+
+ currentState match {
+ case Cached =>
+ // nobody owns the entry, forcefully grab ownership
+ // note: if a new cache lookup is received while
+ // the invalidator has not yet completed (and hence the actual entry
+ // removed from the cache), such lookup operations will still be able
+ // to return the value that is cached, and this is acceptable (under
+ // the eventual consistency model) as long as such lookups do not
+ // mutate the state of the cache to violate the invalidation that is
+ // about to occur (this is eventually consistent and NOT sequentially
+ // consistent since the cache lookup and the setting of the
+ // InvalidateInProgress bit are not atomic
+ invalidateEntryAfter(invalidator, key, actualEntry)
+
+ case ReadInProgress | WriteInProgress =>
+ if (actualEntry.trySet(currentState, InvalidateWhenDone)) {
+ // then the pre-existing owner will take care of the invalidation
+ invalidator
+ } else {
+ // the pre-existing reader or writer finished and so must
+ // explicitly invalidate here
+ invalidateEntryAfter(invalidator, key, actualEntry)
+ }
+
+ case InvalidateInProgress =>
+ if (actualEntry == desiredEntry) {
+ // we own the entry, so we are responsible for cleaning it up
+ invalidateEntryAfter(invalidator, key, actualEntry)
+ } else {
+ // someone else requested an invalidation already
+ invalidator
+ }
+
+ case InvalidateWhenDone =>
+ // a pre-existing owner will take care of the invalidation
+ invalidator
}
- }
+ }
+ } else invalidator // not caching
+ }
+
+ /**
+ * This method may initiate a read from the backing store, and potentially stores the result in the cache.
+ */
+ protected def cacheLookup[Wsuper >: W](key: CacheKey, generator: => Future[W], fromCache: Boolean = cacheEnabled)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId,
+ logger: Logging): Future[W] = {
+ if (fromCache) {
+ val promise = Promise[W] // this promise completes with the generator value
+
+ // try inserting our desired entry...
+ val desiredEntry = Entry(transid, ReadInProgress, Some(promise.future))
+ cache(key)(desiredEntry) flatMap { actualEntry =>
+ // ... and see what we get back
+
+ actualEntry.state.get match {
+ case Cached =>
+ logger.debug(this, "cached read")
+ makeNoteOfCacheHit(key)
+ actualEntry.unpack
+
+ case ReadInProgress =>
+ if (actualEntry == desiredEntry) {
+ logger.debug(this, "read initiated");
+ makeNoteOfCacheMiss(key)
+ // updating the cache with the new value is done in the listener
+ // and will complete unless an invalidation request or an intervening
+ // write occur in the meantime
+ listenForReadDone(key, actualEntry, generator, promise)
+ actualEntry.unpack
+ } else {
+ logger.debug(this, "coalesced read")
+ makeNoteOfCacheHit(key)
+ actualEntry.unpack
+ }
- /**
- * We have initiated a write, now handle its completion:
- * 1. either cache the result if there is no intervening delete or update, or
- * 2. invalidate the cache cache because there was an intervening delete or update
- */
- private def listenForWriteDone(key: CacheKey, entry: Entry, generator: => Future[Winfo])(
- implicit ec: ExecutionContext, transid: TransactionId, logger: Logging): Future[Winfo] = {
-
- generator andThen {
- case Success(_) =>
- // if the datastore write was successful, then transition to the Cached state
- logger.debug(this, "write backend part done, now marking cache entry as done")
-
- if (entry.writeDone()) {
- // entry transitioned from WriteInProgress to Cached state
- logger.info(this, s"write all done, caching $key ${entry.state.get}")
- } else {
- // state transition from WriteInProgress to Cached fails so invalidate
- // the entry in the cache
- val prevState = entry.state.get
- if (prevState != InvalidateWhenDone) {
- // this should not happen but could for example during a document
- // update where the "read" that would fetch a previous instance of
- // the document fails because the document does not exist, but the
- // future callback to invalidate the cache entry is delayed; so it
- // is possible the state here is InvalidateInProgress as a result.
- // the end result is to invalidate the entry, which may be unnecessary;
- // so this is a performance hit, not a correctness concern.
- val error = ConcurrentOperationUnderUpdate(prevState)
- logger.warn(this, error.toString)
- } else {
- logger.info(this, s"write done, but invalidating cache entry as requested")
- }
- invalidateEntry(key, entry)
- }
-
- case Failure(_) => invalidateEntry(key, entry) // datastore write failed, invalidate cache entry
+ case WriteInProgress | InvalidateInProgress =>
+ logger.debug(this, "reading around an update in progress")
+ makeNoteOfCacheMiss(key)
+ generator
+ }
+ }
+ } else generator // not caching
+ }
+
+ /**
+ * This method posts an update to the backing store, and potentially stores the result in the cache.
+ */
+ protected def cacheUpdate(doc: W, key: CacheKey, generator: => Future[Winfo])(
+ implicit ec: ExecutionContext,
+ transid: TransactionId,
+ logger: Logging,
+ notifier: Option[CacheChangeNotification]): Future[Winfo] = {
+ if (cacheEnabled) {
+
+ notifier.foreach(_(key))
+
+ // try inserting our desired entry...
+ val desiredEntry = Entry(transid, WriteInProgress, Some(Future.successful(doc)))
+ cache(key)(desiredEntry) flatMap { actualEntry =>
+ // ... and see what we get back
+
+ if (actualEntry == desiredEntry) {
+ // then this transaction won the race to insert a new entry in the cache
+ // and it is responsible for updating the cache entry...
+ logger.info(this, s"write initiated on new cache entry")
+ listenForWriteDone(key, actualEntry, generator)
+ } else {
+ // ... otherwise, some existing entry is in the way, so try to grab a write lock
+ val currentState = actualEntry.state.get
+ val allowedToAssumeCompletion = currentState == Cached || currentState == ReadInProgress
+
+ if (allowedToAssumeCompletion && actualEntry.grabWriteLock(transid, currentState, desiredEntry.unpack)) {
+ // this transaction is now responsible for updating the cache entry
+ logger.info(this, s"write initiated on existing cache entry, invalidating $key, $actualEntry")
+ listenForWriteDone(key, actualEntry, generator)
+ } else {
+ // there is a conflicting operation in progress on this key
+ logger.info(this, s"write-around (i.e., not cached) under $currentState")
+ invalidateEntryAfter(generator, key, actualEntry)
+ }
+ }
+ }
+ } else generator // not caching
+ }
+
+ def cacheSize: Int = cache.size
+
+ /**
+ * This method removes an entry from the cache immediately. You can use this method
+ * if you do not need to perform any updates on the backing store but only to the cache.
+ */
+ protected[database] def removeId(key: CacheKey)(implicit ec: ExecutionContext): Unit = cache.remove(key)
+
+ /**
+ * Log a cache hit
+ *
+ */
+ private def makeNoteOfCacheHit(key: CacheKey)(implicit transid: TransactionId, logger: Logging) = {
+ transid.mark(this, LoggingMarkers.DATABASE_CACHE_HIT, s"[GET] serving from cache: $key")(logger)
+ }
+
+ /**
+ * Log a cache miss
+ *
+ */
+ private def makeNoteOfCacheMiss(key: CacheKey)(implicit transid: TransactionId, logger: Logging) = {
+ transid.mark(this, LoggingMarkers.DATABASE_CACHE_MISS, s"[GET] serving from datastore: $key")(logger)
+ }
+
+ /**
+ * We have initiated a read (in cacheLookup), now handle its completion:
+ * 1. either cache the result if there is no intervening delete or update, or
+ * 2. invalidate the cache because there was an intervening delete or update.
+ */
+ private def listenForReadDone(key: CacheKey, entry: Entry, generator: => Future[W], promise: Promise[W])(
+ implicit ec: ExecutionContext,
+ transid: TransactionId,
+ logger: Logging): Unit = {
+
+ generator onComplete {
+ case Success(value) =>
+ // if the datastore read was successful, then try to transition to the Cached state
+ logger.debug(this, "read backend part done, now marking cache entry as done")
+
+ // always complete the promise for the generator since the read listener is
+ // only created when reading directly from the database (hence, must complete
+ // promise with the generated value)
+ promise success value
+
+ // now update the cache line
+ if (entry.readDone()) {
+ // cache entry is still in ReadInProgress and successful transitioned to Cached
+ // hence the new value is cached; nothing left to do
+ } else {
+ val cachedLineState = entry.state.get
+
+ cachedLineState match {
+ case WriteInProgress | Cached =>
+ // do nothing: if there was a write in progress, the write has not yet
+ // finished, but that operation has assumed ownership of the cache line
+ // and will update it; otherwise the write has completed and the value
+ // is now cached
+ ()
+ case _ =>
+ // some transaction requested an invalidation so remove the key from the cache,
+ // or there is an error in which case invalidate anyway, defensively, but log a message
+ invalidateEntry(key, entry)
+ if (cachedLineState != InvalidateWhenDone) {
+ // this should not happen, but could if the callback on the generator
+ // is delayed - invalidate the cache entry as a result
+ val error = ConcurrentOperationUnderRead(cachedLineState)
+ logger.warn(this, error.toString)
+ }
+ }
}
- }
- /** Immediately invalidates the given entry. */
- private def invalidateEntry(key: CacheKey, entry: Entry)(
- implicit transid: TransactionId, logger: Logging): Unit = {
- logger.info(this, s"invalidating $key")
- entry.invalidate()
- cache remove key
+ case Failure(t) =>
+ // oops, the datastore read failed. invalidate the cache entry
+ // note: that this might be a perfectly legitimate failure,
+ // e.g. a lookup for a non-existant key; we need to pass the particular t through
+ invalidateEntry(key, entry)
+ promise.failure(t)
}
-
- /** Invalidates the given entry after a given invalidator completes. */
- private def invalidateEntryAfter[R](invalidator: => Future[R], key: CacheKey, entry: Entry)(
- implicit ec: ExecutionContext, transid: TransactionId, logger: Logging): Future[R] = {
-
- entry.grabInvalidationLock()
- invalidator andThen {
- case _ => invalidateEntry(key, entry)
+ }
+
+ /**
+ * We have initiated a write, now handle its completion:
+ * 1. either cache the result if there is no intervening delete or update, or
+ * 2. invalidate the cache cache because there was an intervening delete or update
+ */
+ private def listenForWriteDone(key: CacheKey, entry: Entry, generator: => Future[Winfo])(
+ implicit ec: ExecutionContext,
+ transid: TransactionId,
+ logger: Logging): Future[Winfo] = {
+
+ generator andThen {
+ case Success(_) =>
+ // if the datastore write was successful, then transition to the Cached state
+ logger.debug(this, "write backend part done, now marking cache entry as done")
+
+ if (entry.writeDone()) {
+ // entry transitioned from WriteInProgress to Cached state
+ logger.info(this, s"write all done, caching $key ${entry.state.get}")
+ } else {
+ // state transition from WriteInProgress to Cached fails so invalidate
+ // the entry in the cache
+ val prevState = entry.state.get
+ if (prevState != InvalidateWhenDone) {
+ // this should not happen but could for example during a document
+ // update where the "read" that would fetch a previous instance of
+ // the document fails because the document does not exist, but the
+ // future callback to invalidate the cache entry is delayed; so it
+ // is possible the state here is InvalidateInProgress as a result.
+ // the end result is to invalidate the entry, which may be unnecessary;
+ // so this is a performance hit, not a correctness concern.
+ val error = ConcurrentOperationUnderUpdate(prevState)
+ logger.warn(this, error.toString)
+ } else {
+ logger.info(this, s"write done, but invalidating cache entry as requested")
+ }
+ invalidateEntry(key, entry)
}
- }
- /** This is the backing store. */
- private val cache: ConcurrentMapBackedCache[Entry] = new ConcurrentMapBackedCache(
- Caffeine.newBuilder().asInstanceOf[Caffeine[Any, Future[Entry]]]
- .expireAfterWrite(5, TimeUnit.MINUTES)
- .softValues()
- .build().asMap())
+ case Failure(_) => invalidateEntry(key, entry) // datastore write failed, invalidate cache entry
+ }
+ }
+
+ /** Immediately invalidates the given entry. */
+ private def invalidateEntry(key: CacheKey, entry: Entry)(implicit transid: TransactionId, logger: Logging): Unit = {
+ logger.info(this, s"invalidating $key")
+ entry.invalidate()
+ cache remove key
+ }
+
+ /** Invalidates the given entry after a given invalidator completes. */
+ private def invalidateEntryAfter[R](invalidator: => Future[R], key: CacheKey, entry: Entry)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId,
+ logger: Logging): Future[R] = {
+
+ entry.grabInvalidationLock()
+ invalidator andThen {
+ case _ => invalidateEntry(key, entry)
+ }
+ }
+
+ /** This is the backing store. */
+ private val cache: ConcurrentMapBackedCache[Entry] = new ConcurrentMapBackedCache(
+ Caffeine
+ .newBuilder()
+ .asInstanceOf[Caffeine[Any, Future[Entry]]]
+ .expireAfterWrite(5, TimeUnit.MINUTES)
+ .softValues()
+ .build()
+ .asMap())
}
/**
@@ -444,37 +462,41 @@ trait MultipleReadersSingleWriterCache[W, Winfo] {
* Implementation otherwise is identical.
*/
private class ConcurrentMapBackedCache[V](store: ConcurrentMap[Any, Future[V]]) {
- val cache = this
-
- def apply(key: Any) = new Keyed(key)
-
- class Keyed(key: Any) {
- def apply(magnet: => ValueMagnet[V])(implicit ec: ExecutionContext): Future[V] =
- cache.apply(key, () => try magnet.future catch { case NonFatal(e) => Future.failed(e) })
- }
-
- def apply(key: Any, genValue: () => Future[V])(implicit ec: ExecutionContext): Future[V] = {
- val promise = Promise[V]()
- store.putIfAbsent(key, promise.future) match {
- case null =>
- val future = genValue()
- future.onComplete { value =>
- promise.complete(value)
- // in case of exceptions we remove the cache entry (i.e. try again later)
- if (value.isFailure) store.remove(key, promise.future)
- }
- future
- case existingFuture => existingFuture
+ val cache = this
+
+ def apply(key: Any) = new Keyed(key)
+
+ class Keyed(key: Any) {
+ def apply(magnet: => ValueMagnet[V])(implicit ec: ExecutionContext): Future[V] =
+ cache.apply(
+ key,
+ () =>
+ try magnet.future
+ catch { case NonFatal(e) => Future.failed(e) })
+ }
+
+ def apply(key: Any, genValue: () => Future[V])(implicit ec: ExecutionContext): Future[V] = {
+ val promise = Promise[V]()
+ store.putIfAbsent(key, promise.future) match {
+ case null =>
+ val future = genValue()
+ future.onComplete { value =>
+ promise.complete(value)
+ // in case of exceptions we remove the cache entry (i.e. try again later)
+ if (value.isFailure) store.remove(key, promise.future)
}
+ future
+ case existingFuture => existingFuture
}
+ }
- def remove(key: Any) = Option(store.remove(key))
+ def remove(key: Any) = Option(store.remove(key))
- def size = store.size
+ def size = store.size
}
class ValueMagnet[V](val future: Future[V])
object ValueMagnet {
- implicit def fromAny[V](block: V): ValueMagnet[V] = fromFuture(Future.successful(block))
- implicit def fromFuture[V](future: Future[V]): ValueMagnet[V] = new ValueMagnet(future)
+ implicit def fromAny[V](block: V): ValueMagnet[V] = fromFuture(Future.successful(block))
+ implicit def fromFuture[V](future: Future[V]): ValueMagnet[V] = new ValueMagnet(future)
}
diff --git a/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala b/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
index 621feb6..426b602 100644
--- a/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
+++ b/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
@@ -42,47 +42,54 @@ import whisk.core.entity.WhiskTrigger
import whisk.spi.SpiLoader
case class CacheInvalidationMessage(key: CacheKey, instanceId: String) extends Message {
- override def serialize = CacheInvalidationMessage.serdes.write(this).compactPrint
+ override def serialize = CacheInvalidationMessage.serdes.write(this).compactPrint
}
object CacheInvalidationMessage extends DefaultJsonProtocol {
- def parse(msg: String) = Try(serdes.read(msg.parseJson))
- implicit val serdes = jsonFormat(CacheInvalidationMessage.apply _, "key", "instanceId")
+ def parse(msg: String) = Try(serdes.read(msg.parseJson))
+ implicit val serdes = jsonFormat(CacheInvalidationMessage.apply _, "key", "instanceId")
}
-class RemoteCacheInvalidation(config: WhiskConfig, component: String, instance: InstanceId)(implicit logging: Logging, as: ActorSystem) {
+class RemoteCacheInvalidation(config: WhiskConfig, component: String, instance: InstanceId)(implicit logging: Logging,
+ as: ActorSystem) {
- implicit private val ec = as.dispatcher
+ implicit private val ec = as.dispatcher
- private val topic = "cacheInvalidation"
- private val instanceId = s"$component${instance.toInt}"
+ private val topic = "cacheInvalidation"
+ private val instanceId = s"$component${instance.toInt}"
- private val msgProvider = SpiLoader.get[MessagingProvider]
- private val cacheInvalidationConsumer = msgProvider.getConsumer(config, s"$topic$instanceId", topic, maxPeek = 128)
- private val cacheInvalidationProducer = msgProvider.getProducer(config, ec)
+ private val msgProvider = SpiLoader.get[MessagingProvider]
+ private val cacheInvalidationConsumer = msgProvider.getConsumer(config, s"$topic$instanceId", topic, maxPeek = 128)
+ private val cacheInvalidationProducer = msgProvider.getProducer(config, ec)
- def notifyOtherInstancesAboutInvalidation(key: CacheKey): Future[Unit] = {
- cacheInvalidationProducer.send(topic, CacheInvalidationMessage(key, instanceId)).map(_ => Unit)
- }
+ def notifyOtherInstancesAboutInvalidation(key: CacheKey): Future[Unit] = {
+ cacheInvalidationProducer.send(topic, CacheInvalidationMessage(key, instanceId)).map(_ => Unit)
+ }
- private val invalidationFeed = as.actorOf(Props {
- new MessageFeed("cacheInvalidation", logging, cacheInvalidationConsumer, cacheInvalidationConsumer.maxPeek, 1.second, removeFromLocalCache)
- })
+ private val invalidationFeed = as.actorOf(Props {
+ new MessageFeed(
+ "cacheInvalidation",
+ logging,
+ cacheInvalidationConsumer,
+ cacheInvalidationConsumer.maxPeek,
+ 1.second,
+ removeFromLocalCache)
+ })
- private def removeFromLocalCache(bytes: Array[Byte]): Future[Unit] = Future {
- val raw = new String(bytes, StandardCharsets.UTF_8)
+ private def removeFromLocalCache(bytes: Array[Byte]): Future[Unit] = Future {
+ val raw = new String(bytes, StandardCharsets.UTF_8)
- CacheInvalidationMessage.parse(raw) match {
- case Success(msg: CacheInvalidationMessage) => {
- if (msg.instanceId != instanceId) {
- WhiskAction.removeId(msg.key)
- WhiskPackage.removeId(msg.key)
- WhiskRule.removeId(msg.key)
- WhiskTrigger.removeId(msg.key)
- }
- }
- case Failure(t) => logging.error(this, s"failed processing message: $raw with $t")
+ CacheInvalidationMessage.parse(raw) match {
+ case Success(msg: CacheInvalidationMessage) => {
+ if (msg.instanceId != instanceId) {
+ WhiskAction.removeId(msg.key)
+ WhiskPackage.removeId(msg.key)
+ WhiskRule.removeId(msg.key)
+ WhiskTrigger.removeId(msg.key)
}
- invalidationFeed ! MessageFeed.Processed
+ }
+ case Failure(t) => logging.error(this, s"failed processing message: $raw with $t")
}
+ invalidationFeed ! MessageFeed.Processed
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entitlement/Privilege.scala b/common/scala/src/main/scala/whisk/core/entitlement/Privilege.scala
index b36d75f..06668da 100644
--- a/common/scala/src/main/scala/whisk/core/entitlement/Privilege.scala
+++ b/common/scala/src/main/scala/whisk/core/entitlement/Privilege.scala
@@ -26,21 +26,22 @@ import spray.json.RootJsonFormat
/** An enumeration of privileges available to subjects. */
protected[core] object Privilege extends Enumeration {
- type Privilege = Value
+ type Privilege = Value
- val READ, PUT, DELETE, ACTIVATE, REJECT = Value
+ val READ, PUT, DELETE, ACTIVATE, REJECT = Value
- val CRUD = Set(READ, PUT, DELETE)
- val ALL = CRUD + ACTIVATE
+ val CRUD = Set(READ, PUT, DELETE)
+ val ALL = CRUD + ACTIVATE
- implicit val serdes = new RootJsonFormat[Privilege] {
- def write(p: Privilege) = JsString(p.toString)
+ implicit val serdes = new RootJsonFormat[Privilege] {
+ def write(p: Privilege) = JsString(p.toString)
- def read(json: JsValue) = Try {
- val JsString(str) = json
- Privilege.withName(str.trim.toUpperCase)
- } getOrElse {
- throw new DeserializationException("Privilege must be a valid string")
- }
- }
+ def read(json: JsValue) =
+ Try {
+ val JsString(str) = json
+ Privilege.withName(str.trim.toUpperCase)
+ } getOrElse {
+ throw new DeserializationException("Privilege must be a valid string")
+ }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/ActivationEntityLimit.scala b/common/scala/src/main/scala/whisk/core/entity/ActivationEntityLimit.scala
index e40df82..5c149db 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ActivationEntityLimit.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ActivationEntityLimit.scala
@@ -25,5 +25,5 @@ import whisk.core.entity.size.SizeInt
* parameters for triggers.
*/
protected[core] object ActivationEntityLimit {
- protected[core] val MAX_ACTIVATION_ENTITY_LIMIT = 1.MB
+ protected[core] val MAX_ACTIVATION_ENTITY_LIMIT = 1.MB
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/ActivationId.scala b/common/scala/src/main/scala/whisk/core/entity/ActivationId.scala
index e94ebaf..4ad917f 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ActivationId.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ActivationId.scala
@@ -41,93 +41,94 @@ import whisk.http.Messages
* @param id the activation id, required not null
*/
protected[whisk] class ActivationId private (private val id: java.util.UUID) extends AnyVal {
- def asString = toString
- override def toString = id.toString.replaceAll("-", "")
- def toJsObject = JsObject("activationId" -> toString.toJson)
+ def asString = toString
+ override def toString = id.toString.replaceAll("-", "")
+ def toJsObject = JsObject("activationId" -> toString.toJson)
}
protected[core] object ActivationId extends ArgNormalizer[ActivationId] {
- protected[core] trait ActivationIdGenerator {
- def make(): ActivationId = new ActivationId(java.util.UUID.randomUUID())
- }
-
- /**
- * Unapply method for convenience of case matching.
- */
- protected[core] def unapply(name: String): Option[ActivationId] = {
- Try { ActivationId(name) } toOption
- }
-
- /**
- * Creates an activation id from a java.util.UUID.
- *
- * @param uuid the activation id as UUID
- * @return ActivationId instance
- * @throws IllegalArgumentException is argument is not defined
- */
- @throws[IllegalArgumentException]
- private def apply(uuid: java.util.UUID): ActivationId = {
- require(uuid != null, "argument undefined")
- new ActivationId(uuid)
- }
-
- /**
- * Generates a random activation id using java.util.UUID factory.
- *
- * @return new ActivationId
- */
- protected[core] def apply(): ActivationId = new ActivationId(java.util.UUID.randomUUID())
-
- /**
- * Overrides factory method so that string is not interpreted as number
- * e.g., 2e11.
- */
- override protected[entity] def factory(s: String): ActivationId = {
- serdes.read(JsString(s))
- }
-
- override protected[core] implicit val serdes = new RootJsonFormat[ActivationId] {
- def write(d: ActivationId) = JsString(d.toString)
-
- def read(value: JsValue) = Try {
- value match {
- case JsString(s) => stringToActivationId(s)
- case JsNumber(n) => bigIntToActivationId(n.toBigInt)
- case _ => deserializationError(Messages.activationIdIllegal)
- }
- } match {
- case Success(a) => a
- case Failure(DeserializationException(t, _, _)) => deserializationError(t)
- case Failure(t) => deserializationError(Messages.activationIdIllegal)
+ protected[core] trait ActivationIdGenerator {
+ def make(): ActivationId = new ActivationId(java.util.UUID.randomUUID())
+ }
+
+ /**
+ * Unapply method for convenience of case matching.
+ */
+ protected[core] def unapply(name: String): Option[ActivationId] = {
+ Try { ActivationId(name) } toOption
+ }
+
+ /**
+ * Creates an activation id from a java.util.UUID.
+ *
+ * @param uuid the activation id as UUID
+ * @return ActivationId instance
+ * @throws IllegalArgumentException is argument is not defined
+ */
+ @throws[IllegalArgumentException]
+ private def apply(uuid: java.util.UUID): ActivationId = {
+ require(uuid != null, "argument undefined")
+ new ActivationId(uuid)
+ }
+
+ /**
+ * Generates a random activation id using java.util.UUID factory.
+ *
+ * @return new ActivationId
+ */
+ protected[core] def apply(): ActivationId = new ActivationId(java.util.UUID.randomUUID())
+
+ /**
+ * Overrides factory method so that string is not interpreted as number
+ * e.g., 2e11.
+ */
+ override protected[entity] def factory(s: String): ActivationId = {
+ serdes.read(JsString(s))
+ }
+
+ override protected[core] implicit val serdes = new RootJsonFormat[ActivationId] {
+ def write(d: ActivationId) = JsString(d.toString)
+
+ def read(value: JsValue) =
+ Try {
+ value match {
+ case JsString(s) => stringToActivationId(s)
+ case JsNumber(n) => bigIntToActivationId(n.toBigInt)
+ case _ => deserializationError(Messages.activationIdIllegal)
}
- }
-
- private def bigIntToActivationId(n: BigInt): ActivationId = {
- // print the bigint using base 10 then convert to base 16
- val bn = new BigInteger(n.bigInteger.toString(10), 16)
- // mask out the upper 16 ints
- val lb = bn.and(new BigInteger("f" * 16, 16))
- // drop the lower 16 ints
- val up = bn.shiftRight(16)
+ } match {
+ case Success(a) => a
+ case Failure(DeserializationException(t, _, _)) => deserializationError(t)
+ case Failure(t) => deserializationError(Messages.activationIdIllegal)
+ }
+ }
+
+ private def bigIntToActivationId(n: BigInt): ActivationId = {
+ // print the bigint using base 10 then convert to base 16
+ val bn = new BigInteger(n.bigInteger.toString(10), 16)
+ // mask out the upper 16 ints
+ val lb = bn.and(new BigInteger("f" * 16, 16))
+ // drop the lower 16 ints
+ val up = bn.shiftRight(16)
+ val uuid = new java.util.UUID(lb.longValue, up.longValue)
+ ActivationId(uuid)
+ }
+
+ private def stringToActivationId(s: String): ActivationId = {
+ if (!s.contains("-")) {
+ if (s.length == 32) {
+ val lb = new BigInteger(s.substring(0, 16), 16)
+ val up = new BigInteger(s.substring(16, 32), 16)
val uuid = new java.util.UUID(lb.longValue, up.longValue)
ActivationId(uuid)
- }
-
- private def stringToActivationId(s: String): ActivationId = {
- if (!s.contains("-")) {
- if (s.length == 32) {
- val lb = new BigInteger(s.substring(0, 16), 16)
- val up = new BigInteger(s.substring(16, 32), 16)
- val uuid = new java.util.UUID(lb.longValue, up.longValue)
- ActivationId(uuid)
- } else deserializationError {
- Messages.activationIdLengthError(
- SizeError("Activation id", s.length.B, 32.B))
- }
- } else {
- ActivationId(java.util.UUID.fromString(s))
+ } else
+ deserializationError {
+ Messages.activationIdLengthError(SizeError("Activation id", s.length.B, 32.B))
}
-
+ } else {
+ ActivationId(java.util.UUID.fromString(s))
}
+
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/ActivationLogs.scala b/common/scala/src/main/scala/whisk/core/entity/ActivationLogs.scala
index 08f4835..e2c9e07 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ActivationLogs.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ActivationLogs.scala
@@ -30,22 +30,23 @@ import spray.json.deserializationError
import spray.json.pimpAny
protected[core] case class ActivationLogs(val logs: Vector[String] = Vector()) extends AnyVal {
- def toJsonObject = JsObject("logs" -> toJson)
- def toJson = JsArray(logs map { _.toJson })
+ def toJsonObject = JsObject("logs" -> toJson)
+ def toJson = JsArray(logs map { _.toJson })
- override def toString = logs mkString ("[", ", ", "]")
+ override def toString = logs mkString ("[", ", ", "]")
}
protected[core] object ActivationLogs {
- protected[core] implicit val serdes = new RootJsonFormat[ActivationLogs] {
- def write(l: ActivationLogs) = l.toJson
+ protected[core] implicit val serdes = new RootJsonFormat[ActivationLogs] {
+ def write(l: ActivationLogs) = l.toJson
- def read(value: JsValue) = Try {
- val JsArray(logs) = value
- ActivationLogs(logs map {
- case JsString(s) => s
- case _ => deserializationError("activation logs malformed")
- })
- } getOrElse deserializationError("activation logs malformed")
- }
+ def read(value: JsValue) =
+ Try {
+ val JsArray(logs) = value
+ ActivationLogs(logs map {
+ case JsString(s) => s
+ case _ => deserializationError("activation logs malformed")
+ })
+ } getOrElse deserializationError("activation logs malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/ActivationResult.scala b/common/scala/src/main/scala/whisk/core/entity/ActivationResult.scala
index d700062..495bbc0 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ActivationResult.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ActivationResult.scala
@@ -27,181 +27,190 @@ import spray.json.DefaultJsonProtocol
import whisk.common.Logging
import whisk.http.Messages._
-protected[core] case class ActivationResponse private (
- val statusCode: Int, val result: Option[JsValue]) {
+protected[core] case class ActivationResponse private (val statusCode: Int, val result: Option[JsValue]) {
- def toJsonObject = ActivationResponse.serdes.write(this).asJsObject
+ def toJsonObject = ActivationResponse.serdes.write(this).asJsObject
- // Used when presenting to end-users, to hide the statusCode (which is an implementation detail),
- // and to provide a convenience boolean "success" field.
- def toExtendedJson: JsObject = {
- val baseFields = this.toJsonObject.fields
- JsObject((baseFields - "statusCode") ++ Seq(
- "success" -> JsBoolean(this.isSuccess),
- "status" -> JsString(ActivationResponse.messageForCode(statusCode))))
+ // Used when presenting to end-users, to hide the statusCode (which is an implementation detail),
+ // and to provide a convenience boolean "success" field.
+ def toExtendedJson: JsObject = {
+ val baseFields = this.toJsonObject.fields
+ JsObject(
+ (baseFields - "statusCode") ++ Seq(
+ "success" -> JsBoolean(this.isSuccess),
+ "status" -> JsString(ActivationResponse.messageForCode(statusCode))))
- }
+ }
- def isSuccess = statusCode == ActivationResponse.Success
- def isApplicationError = statusCode == ActivationResponse.ApplicationError
- def isContainerError = statusCode == ActivationResponse.ContainerError
- def isWhiskError = statusCode == ActivationResponse.WhiskError
- def withoutResult = ActivationResponse(statusCode, None)
+ def isSuccess = statusCode == ActivationResponse.Success
+ def isApplicationError = statusCode == ActivationResponse.ApplicationError
+ def isContainerError = statusCode == ActivationResponse.ContainerError
+ def isWhiskError = statusCode == ActivationResponse.WhiskError
+ def withoutResult = ActivationResponse(statusCode, None)
- override def toString = toJsonObject.compactPrint
+ override def toString = toJsonObject.compactPrint
}
protected[core] object ActivationResponse extends DefaultJsonProtocol {
- /* The field name that is universally recognized as the marker of an error, from the application or otherwise. */
- val ERROR_FIELD: String = "error"
-
- val Success = 0 // action ran successfully and produced a result
- val ApplicationError = 1 // action ran but there was an error and it was handled
- val ContainerError = 2 // action ran but failed to handle an error, or action did not run and failed to initialize
- val WhiskError = 3 // internal system error
-
- protected[core] def messageForCode(code: Int) = {
- require(code >= 0 && code <= 3)
- code match {
- case 0 => "success"
- case 1 => "application error"
- case 2 => "action developer error"
- case 3 => "whisk internal error"
- }
+ /* The field name that is universally recognized as the marker of an error, from the application or otherwise. */
+ val ERROR_FIELD: String = "error"
+
+ val Success = 0 // action ran successfully and produced a result
+ val ApplicationError = 1 // action ran but there was an error and it was handled
+ val ContainerError = 2 // action ran but failed to handle an error, or action did not run and failed to initialize
+ val WhiskError = 3 // internal system error
+
+ protected[core] def messageForCode(code: Int) = {
+ require(code >= 0 && code <= 3)
+ code match {
+ case 0 => "success"
+ case 1 => "application error"
+ case 2 => "action developer error"
+ case 3 => "whisk internal error"
}
-
- private def error(code: Int, errorValue: JsValue) = {
- require(code == ApplicationError || code == ContainerError || code == WhiskError)
- ActivationResponse(code, Some(JsObject(ERROR_FIELD -> errorValue)))
+ }
+
+ private def error(code: Int, errorValue: JsValue) = {
+ require(code == ApplicationError || code == ContainerError || code == WhiskError)
+ ActivationResponse(code, Some(JsObject(ERROR_FIELD -> errorValue)))
+ }
+
+ protected[core] def success(result: Option[JsValue] = None) = ActivationResponse(Success, result)
+
+ protected[core] def applicationError(errorValue: JsValue) = error(ApplicationError, errorValue)
+ protected[core] def applicationError(errorMsg: String) = error(ApplicationError, JsString(errorMsg))
+ protected[core] def containerError(errorValue: JsValue) = error(ContainerError, errorValue)
+ protected[core] def containerError(errorMsg: String) = error(ContainerError, JsString(errorMsg))
+ protected[core] def whiskError(errorValue: JsValue) = error(WhiskError, errorValue)
+ protected[core] def whiskError(errorMsg: String) = error(WhiskError, JsString(errorMsg))
+
+ /**
+ * Returns an ActivationResponse that is used as a placeholder for payload
+ * Used as a feed for starting a sequence.
+ * NOTE: the code is application error (since this response could be used as a response for the sequence
+ * if the payload contains an error)
+ */
+ protected[core] def payloadPlaceholder(payload: Option[JsObject]) = ActivationResponse(ApplicationError, payload)
+
+ /**
+ * Class of errors for invoker-container communication.
+ */
+ protected[core] sealed abstract class ContainerConnectionError
+ protected[core] case class NoHost() extends ContainerConnectionError
+ protected[core] case class ConnectionError(t: Throwable) extends ContainerConnectionError
+ protected[core] case class NoResponseReceived() extends ContainerConnectionError
+ protected[core] case class Timeout() extends ContainerConnectionError
+
+ /**
+ * @param statusCode the container HTTP response code (e.g., 200 OK)
+ * @param entity the entity response as string
+ * @param truncated either None to indicate complete entity or Some(actual length, max allowed)
+ */
+ protected[core] case class ContainerResponse(statusCode: Int,
+ entity: String,
+ truncated: Option[(ByteSize, ByteSize)]) {
+
+ /** true iff status code is OK (HTTP 200 status code), anything else is considered an error. **/
+ val okStatus = statusCode == OK.intValue
+ val ok = okStatus && truncated.isEmpty
+ override def toString = {
+ val base = if (okStatus) "ok" else "not ok"
+ val rest = truncated.map(e => s", truncated ${e.toString}").getOrElse("")
+ base + rest
}
+ }
- protected[core] def success(result: Option[JsValue] = None) = ActivationResponse(Success, result)
-
- protected[core] def applicationError(errorValue: JsValue) = error(ApplicationError, errorValue)
- protected[core] def applicationError(errorMsg: String) = error(ApplicationError, JsString(errorMsg))
- protected[core] def containerError(errorValue: JsValue) = error(ContainerError, errorValue)
- protected[core] def containerError(errorMsg: String) = error(ContainerError, JsString(errorMsg))
- protected[core] def whiskError(errorValue: JsValue) = error(WhiskError, errorValue)
- protected[core] def whiskError(errorMsg: String) = error(WhiskError, JsString(errorMsg))
-
- /**
- * Returns an ActivationResponse that is used as a placeholder for payload
- * Used as a feed for starting a sequence.
- * NOTE: the code is application error (since this response could be used as a response for the sequence
- * if the payload contains an error)
- */
- protected[core] def payloadPlaceholder(payload: Option[JsObject]) = ActivationResponse(ApplicationError, payload)
-
- /**
- * Class of errors for invoker-container communication.
- */
- protected[core] sealed abstract class ContainerConnectionError
- protected[core] case class NoHost() extends ContainerConnectionError
- protected[core] case class ConnectionError(t: Throwable) extends ContainerConnectionError
- protected[core] case class NoResponseReceived() extends ContainerConnectionError
- protected[core] case class Timeout() extends ContainerConnectionError
-
- /**
- * @param statusCode the container HTTP response code (e.g., 200 OK)
- * @param entity the entity response as string
- * @param truncated either None to indicate complete entity or Some(actual length, max allowed)
- */
- protected[core] case class ContainerResponse(statusCode: Int, entity: String, truncated: Option[(ByteSize, ByteSize)]) {
- /** true iff status code is OK (HTTP 200 status code), anything else is considered an error. **/
- val okStatus = statusCode == OK.intValue
- val ok = okStatus && truncated.isEmpty
- override def toString = {
- val base = if (okStatus) "ok" else "not ok"
- val rest = truncated.map(e => s", truncated ${e.toString}").getOrElse("")
- base + rest
- }
+ protected[core] object ContainerResponse {
+ def apply(okStatus: Boolean, entity: String, truncated: Option[(ByteSize, ByteSize)] = None): ContainerResponse = {
+ ContainerResponse(if (okStatus) OK.intValue else 500, entity, truncated)
}
-
- protected[core] object ContainerResponse {
- def apply(okStatus: Boolean, entity: String, truncated: Option[(ByteSize, ByteSize)] = None): ContainerResponse = {
- ContainerResponse(if (okStatus) OK.intValue else 500, entity, truncated)
- }
- }
-
- /**
- * Interprets response from container after initialization. This method is only called when the initialization failed.
- *
- * @param response an either a container error or container response (HTTP Status Code, HTTP response bytes as String)
- * @return appropriate ActivationResponse representing initialization error
- */
- protected[core] def processInitResponseContent(response: Either[ContainerConnectionError, ContainerResponse], logger: Logging): ActivationResponse = {
- require(response.isLeft || !response.right.exists(_.ok), s"should not interpret init response when status code is OK")
- response match {
- case Right(ContainerResponse(code, str, truncated)) => truncated match {
- case None =>
- Try { str.parseJson.asJsObject } match {
- case scala.util.Success(result @ JsObject(fields)) =>
- // If the response is a JSON object container an error field, accept it as the response error.
- val errorOpt = fields.get(ERROR_FIELD)
- val errorContent = errorOpt getOrElse invalidInitResponse(str).toJson
- containerError(errorContent)
- case _ =>
- containerError(invalidInitResponse(str))
- }
-
- case Some((length, maxlength)) =>
- containerError(truncatedResponse(str, length, maxlength))
+ }
+
+ /**
+ * Interprets response from container after initialization. This method is only called when the initialization failed.
+ *
+ * @param response an either a container error or container response (HTTP Status Code, HTTP response bytes as String)
+ * @return appropriate ActivationResponse representing initialization error
+ */
+ protected[core] def processInitResponseContent(response: Either[ContainerConnectionError, ContainerResponse],
+ logger: Logging): ActivationResponse = {
+ require(
+ response.isLeft || !response.right.exists(_.ok),
+ s"should not interpret init response when status code is OK")
+ response match {
+ case Right(ContainerResponse(code, str, truncated)) =>
+ truncated match {
+ case None =>
+ Try { str.parseJson.asJsObject } match {
+ case scala.util.Success(result @ JsObject(fields)) =>
+ // If the response is a JSON object container an error field, accept it as the response error.
+ val errorOpt = fields.get(ERROR_FIELD)
+ val errorContent = errorOpt getOrElse invalidInitResponse(str).toJson
+ containerError(errorContent)
+ case _ =>
+ containerError(invalidInitResponse(str))
}
- case Left(e) =>
- // This indicates a terminal failure in the container (it exited prematurely).
- containerError(abnormalInitialization)
+ case Some((length, maxlength)) =>
+ containerError(truncatedResponse(str, length, maxlength))
}
- }
- /**
- * Interprets response from container after running the action. This method is only called when the initialization succeeded.
- *
- * @param response an Option (HTTP Status Code, HTTP response bytes as String)
- * @return appropriate ActivationResponse representing run result
- */
- protected[core] def processRunResponseContent(response: Either[ContainerConnectionError, ContainerResponse], logger: Logging): ActivationResponse = {
- response match {
- case Right(res @ ContainerResponse(_, str, truncated)) => truncated match {
- case None =>
- Try { str.parseJson.asJsObject } match {
- case scala.util.Success(result @ JsObject(fields)) =>
- // If the response is a JSON object container an error field, accept it as the response error.
- val errorOpt = fields.get(ERROR_FIELD)
-
- if (res.okStatus) {
- errorOpt map { error =>
- applicationError(error)
- } getOrElse {
- // The happy path.
- success(Some(result))
- }
- } else {
- // Any non-200 code is treated as a container failure. We still need to check whether
- // there was a useful error message in there.
- val errorContent = errorOpt getOrElse invalidRunResponse(str).toJson
- containerError(errorContent)
- }
-
- case scala.util.Success(notAnObj) =>
- // This should affect only blackbox containers, since our own containers should already test for that.
- containerError(invalidRunResponse(str))
-
- case scala.util.Failure(t) =>
- // This should affect only blackbox containers, since our own containers should already test for that.
- logger.warn(this, s"response did not json parse: '$str' led to $t")
- containerError(invalidRunResponse(str))
- }
-
- case Some((length, maxlength)) =>
- containerError(truncatedResponse(str, length, maxlength))
+ case Left(e) =>
+ // This indicates a terminal failure in the container (it exited prematurely).
+ containerError(abnormalInitialization)
+ }
+ }
+
+ /**
+ * Interprets response from container after running the action. This method is only called when the initialization succeeded.
+ *
+ * @param response an Option (HTTP Status Code, HTTP response bytes as String)
+ * @return appropriate ActivationResponse representing run result
+ */
+ protected[core] def processRunResponseContent(response: Either[ContainerConnectionError, ContainerResponse],
+ logger: Logging): ActivationResponse = {
+ response match {
+ case Right(res @ ContainerResponse(_, str, truncated)) =>
+ truncated match {
+ case None =>
+ Try { str.parseJson.asJsObject } match {
+ case scala.util.Success(result @ JsObject(fields)) =>
+ // If the response is a JSON object container an error field, accept it as the response error.
+ val errorOpt = fields.get(ERROR_FIELD)
+
+ if (res.okStatus) {
+ errorOpt map { error =>
+ applicationError(error)
+ } getOrElse {
+ // The happy path.
+ success(Some(result))
+ }
+ } else {
+ // Any non-200 code is treated as a container failure. We still need to check whether
+ // there was a useful error message in there.
+ val errorContent = errorOpt getOrElse invalidRunResponse(str).toJson
+ containerError(errorContent)
+ }
+
+ case scala.util.Success(notAnObj) =>
+ // This should affect only blackbox containers, since our own containers should already test for that.
+ containerError(invalidRunResponse(str))
+
+ case scala.util.Failure(t) =>
+ // This should affect only blackbox containers, since our own containers should already test for that.
+ logger.warn(this, s"response did not json parse: '$str' led to $t")
+ containerError(invalidRunResponse(str))
}
- case Left(e) =>
- // This indicates a terminal failure in the container (it exited prematurely).
- containerError(abnormalRun)
+ case Some((length, maxlength)) =>
+ containerError(truncatedResponse(str, length, maxlength))
}
+
+ case Left(e) =>
+ // This indicates a terminal failure in the container (it exited prematurely).
+ containerError(abnormalRun)
}
+ }
- protected[core] implicit val serdes = jsonFormat2(ActivationResponse.apply)
+ protected[core] implicit val serdes = jsonFormat2(ActivationResponse.apply)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/ArgNormalizer.scala b/common/scala/src/main/scala/whisk/core/entity/ArgNormalizer.scala
index da10125..6667639 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ArgNormalizer.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ArgNormalizer.scala
@@ -24,28 +24,28 @@ import scala.util.Try
protected[entity] trait ArgNormalizer[T] {
- protected[core] val serdes: RootJsonFormat[T]
+ protected[core] val serdes: RootJsonFormat[T]
- protected[entity] def factory(s: String): T = {
- serdes.read(Try { s.parseJson } getOrElse JsString(s))
- }
+ protected[entity] def factory(s: String): T = {
+ serdes.read(Try { s.parseJson } getOrElse JsString(s))
+ }
- /**
- * Creates a new T from string. The method checks that a string
- * argument is not null, not empty, and normalizes it by trimming
- * white space before creating new T.
- *
- * @param s is the string argument to supply to factory of T
- * @return T instance
- * @throws IllegalArgumentException if string is null or empty
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(s: String): T = {
- require(s != null && s.trim.nonEmpty, "argument undefined")
- factory(s.trim)
- }
+ /**
+ * Creates a new T from string. The method checks that a string
+ * argument is not null, not empty, and normalizes it by trimming
+ * white space before creating new T.
+ *
+ * @param s is the string argument to supply to factory of T
+ * @return T instance
+ * @throws IllegalArgumentException if string is null or empty
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(s: String): T = {
+ require(s != null && s.trim.nonEmpty, "argument undefined")
+ factory(s.trim)
+ }
}
protected[entity] object ArgNormalizer {
- protected[entity] def trim(s: String) = Option(s) map { _.trim} getOrElse s
+ protected[entity] def trim(s: String) = Option(s) map { _.trim } getOrElse s
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Attachments.scala b/common/scala/src/main/scala/whisk/core/entity/Attachments.scala
index d30b568..d7792cb 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Attachments.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Attachments.scala
@@ -27,60 +27,63 @@ import spray.json.DefaultJsonProtocol._
import whisk.core.entity.size._
object Attachments {
- /**
- * A marker for a field that is either inlined in an entity, or a reference
- * to an attachment. In the case where the value is inlined, it (de)serializes
- * to the same value as if it weren't wrapped.
- *
- * Note that such fields may be defined at any level of nesting in an entity,
- * but the attachments will always be top-level. The logic for actually retrieving
- * an attachment therefore must be separate for all use cases.
- */
- sealed trait Attachment[+T]
- case class Inline[T](value: T) extends Attachment[T]
+ /**
+ * A marker for a field that is either inlined in an entity, or a reference
+ * to an attachment. In the case where the value is inlined, it (de)serializes
+ * to the same value as if it weren't wrapped.
+ *
+ * Note that such fields may be defined at any level of nesting in an entity,
+ * but the attachments will always be top-level. The logic for actually retrieving
+ * an attachment therefore must be separate for all use cases.
+ */
+ sealed trait Attachment[+T]
- case class Attached(attachmentName: String, attachmentType: ContentType) extends Attachment[Nothing]
+ case class Inline[T](value: T) extends Attachment[T]
- // Attachments are considered free because the name/content type are system determined
- // and a size check for the content is done during create/update
- implicit class SizeAttachment[T <% SizeConversion](a: Attachment[T]) extends SizeConversion {
- def sizeIn(unit: SizeUnits.Unit): ByteSize = a match {
- case Inline(v) => (v: SizeConversion).sizeIn(unit)
- case _ => 0.bytes
- }
+ case class Attached(attachmentName: String, attachmentType: ContentType) extends Attachment[Nothing]
+
+ // Attachments are considered free because the name/content type are system determined
+ // and a size check for the content is done during create/update
+ implicit class SizeAttachment[T <% SizeConversion](a: Attachment[T]) extends SizeConversion {
+ def sizeIn(unit: SizeUnits.Unit): ByteSize = a match {
+ case Inline(v) => (v: SizeConversion).sizeIn(unit)
+ case _ => 0.bytes
}
+ }
- object Attached {
- implicit val serdes = {
- implicit val contentTypeSerdes = new RootJsonFormat[ContentType] {
- override def write(c: ContentType) = JsString(c.value)
- override def read(js: JsValue) = Try {
- val JsString(c) = js
- ContentType.parse(c).right.get
- } getOrElse {
- throw new DeserializationException("Could not deserialize content-type")
- }
- }
+ object Attached {
+ implicit val serdes = {
+ implicit val contentTypeSerdes = new RootJsonFormat[ContentType] {
+ override def write(c: ContentType) = JsString(c.value)
+ override def read(js: JsValue) =
+ Try {
+ val JsString(c) = js
+ ContentType.parse(c).right.get
+ } getOrElse {
+ throw new DeserializationException("Could not deserialize content-type")
+ }
+ }
- jsonFormat2(Attached.apply)
- }
+ jsonFormat2(Attached.apply)
}
+ }
- implicit def serdes[T: JsonFormat] = new JsonFormat[Attachment[T]] {
- val sub = implicitly[JsonFormat[T]]
-
- def write(a: Attachment[T]): JsValue = a match {
- case Inline(v) => sub.write(v)
- case a: Attached => Attached.serdes.write(a)
- }
+ implicit def serdes[T: JsonFormat] = new JsonFormat[Attachment[T]] {
+ val sub = implicitly[JsonFormat[T]]
- def read(js: JsValue): Attachment[T] = Try {
- Inline(sub.read(js))
- } recover {
- case _: DeserializationException => Attached.serdes.read(js)
- } getOrElse {
- throw new DeserializationException("Could not deserialize as attachment record: " + js)
- }
+ def write(a: Attachment[T]): JsValue = a match {
+ case Inline(v) => sub.write(v)
+ case a: Attached => Attached.serdes.write(a)
}
+
+ def read(js: JsValue): Attachment[T] =
+ Try {
+ Inline(sub.read(js))
+ } recover {
+ case _: DeserializationException => Attached.serdes.read(js)
+ } getOrElse {
+ throw new DeserializationException("Could not deserialize as attachment record: " + js)
+ }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/AuthKey.scala b/common/scala/src/main/scala/whisk/core/entity/AuthKey.scala
index c718f38..55aa96b 100644
--- a/common/scala/src/main/scala/whisk/core/entity/AuthKey.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/AuthKey.scala
@@ -34,66 +34,67 @@ import spray.json.deserializationError
* @param (uuid, key) the uuid and key, assured to be non-null because both types are values
*/
protected[core] class AuthKey private (private val k: (UUID, Secret)) extends AnyVal {
- def uuid = k._1
- def key = k._2
- def revoke = new AuthKey(uuid, Secret())
- def compact = s"$uuid:$key"
- override def toString = uuid.toString
+ def uuid = k._1
+ def key = k._2
+ def revoke = new AuthKey(uuid, Secret())
+ def compact = s"$uuid:$key"
+ override def toString = uuid.toString
}
protected[core] object AuthKey {
- /**
- * Creates AuthKey.
- *
- * @param uuid the uuid, assured to be non-null because UUID is a value
- * @param key the key, assured to be non-null because Secret is a value
- */
- protected[core] def apply(uuid: UUID, key: Secret): AuthKey = new AuthKey(uuid, key)
+ /**
+ * Creates AuthKey.
+ *
+ * @param uuid the uuid, assured to be non-null because UUID is a value
+ * @param key the key, assured to be non-null because Secret is a value
+ */
+ protected[core] def apply(uuid: UUID, key: Secret): AuthKey = new AuthKey(uuid, key)
- /**
- * Creates an auth key for a randomly generated UUID with a randomly generated secret.
- *
- * @return AuthKey
- */
- protected[core] def apply(): AuthKey = new AuthKey(UUID(), Secret())
+ /**
+ * Creates an auth key for a randomly generated UUID with a randomly generated secret.
+ *
+ * @return AuthKey
+ */
+ protected[core] def apply(): AuthKey = new AuthKey(UUID(), Secret())
- /**
- * Creates AuthKey from a string where the uuid and key are separated by a colon.
- * If the string contains more than one colon, all values are ignored except for
- * the first two hence "k:v*" produces ("k","v").
- *
- * @param str the string containing uuid and key separated by colon
- * @return AuthKey if argument is properly formated
- * @throws IllegalArgumentException if argument is not well formed
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(str: String): AuthKey = {
- val (k, v) = split(str)
- new AuthKey(UUID(k), Secret(v))
- }
+ /**
+ * Creates AuthKey from a string where the uuid and key are separated by a colon.
+ * If the string contains more than one colon, all values are ignored except for
+ * the first two hence "k:v*" produces ("k","v").
+ *
+ * @param str the string containing uuid and key separated by colon
+ * @return AuthKey if argument is properly formated
+ * @throws IllegalArgumentException if argument is not well formed
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(str: String): AuthKey = {
+ val (k, v) = split(str)
+ new AuthKey(UUID(k), Secret(v))
+ }
- /**
- * Makes a tuple from a string where the values are separated by a colon.
- * If the string contains more than one colon, all values are ignored except for
- * the first two hence "k:v*" produces the tuple ("k","v") and "::*" produces ("","").
- *
- * @param string to create pair from
- * @return (key, value) where both are null, value is null, or neither is null
- */
- private def split(str: String): (String, String) = {
- val parts = if (str != null && str.nonEmpty) str.split(":") else Array[String]()
- val k = if (parts.size >= 1) parts(0).trim else null
- val v = if (parts.size == 2) parts(1).trim else null
- (k, v)
- }
+ /**
+ * Makes a tuple from a string where the values are separated by a colon.
+ * If the string contains more than one colon, all values are ignored except for
+ * the first two hence "k:v*" produces the tuple ("k","v") and "::*" produces ("","").
+ *
+ * @param string to create pair from
+ * @return (key, value) where both are null, value is null, or neither is null
+ */
+ private def split(str: String): (String, String) = {
+ val parts = if (str != null && str.nonEmpty) str.split(":") else Array[String]()
+ val k = if (parts.size >= 1) parts(0).trim else null
+ val v = if (parts.size == 2) parts(1).trim else null
+ (k, v)
+ }
- protected[core] implicit val serdes = new RootJsonFormat[AuthKey] {
- def write(k: AuthKey) = JsString(k.compact)
+ protected[core] implicit val serdes = new RootJsonFormat[AuthKey] {
+ def write(k: AuthKey) = JsString(k.compact)
- def read(value: JsValue) = Try {
- val JsString(s) = value
- AuthKey(s)
- } getOrElse deserializationError("authorization key malformed")
- }
+ def read(value: JsValue) =
+ Try {
+ val JsString(s) = value
+ AuthKey(s)
+ } getOrElse deserializationError("authorization key malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/CacheKey.scala b/common/scala/src/main/scala/whisk/core/entity/CacheKey.scala
index de0f6fe..8fc8f83 100644
--- a/common/scala/src/main/scala/whisk/core/entity/CacheKey.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/CacheKey.scala
@@ -29,27 +29,27 @@ class UnsupportedCacheKeyTypeException(msg: String) extends Exception(msg)
* of the key will not be written to the logs.
*/
case class CacheKey(mainId: String, secondaryId: Option[String]) {
- override def toString() = {
- s"CacheKey($mainId)"
- }
+ override def toString() = {
+ s"CacheKey($mainId)"
+ }
}
object CacheKey extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat2(CacheKey.apply)
+ implicit val serdes = jsonFormat2(CacheKey.apply)
- def apply(key: Any): CacheKey = {
- key match {
- case e: EntityName => CacheKey(e.asString, None)
- case a: AuthKey => CacheKey(a.uuid.asString, Some(a.key.asString))
- case d: DocInfo => {
- val revision = if (d.rev.empty) None else Some(d.rev.asString)
- CacheKey(d.id.asString, revision)
- }
- case w: WhiskEntity => CacheKey(w.docid.asDocInfo)
- case s: String => CacheKey(s, None)
- case others => {
- throw new UnsupportedCacheKeyTypeException(s"Unable to apply the entity ${others.getClass} on CacheKey.")
- }
- }
+ def apply(key: Any): CacheKey = {
+ key match {
+ case e: EntityName => CacheKey(e.asString, None)
+ case a: AuthKey => CacheKey(a.uuid.asString, Some(a.key.asString))
+ case d: DocInfo => {
+ val revision = if (d.rev.empty) None else Some(d.rev.asString)
+ CacheKey(d.id.asString, revision)
+ }
+ case w: WhiskEntity => CacheKey(w.docid.asDocInfo)
+ case s: String => CacheKey(s, None)
+ case others => {
+ throw new UnsupportedCacheKeyTypeException(s"Unable to apply the entity ${others.getClass} on CacheKey.")
+ }
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/DocInfo.scala b/common/scala/src/main/scala/whisk/core/entity/DocInfo.scala
index ba5b0b5..c30d903 100644
--- a/common/scala/src/main/scala/whisk/core/entity/DocInfo.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/DocInfo.scala
@@ -35,11 +35,11 @@ import spray.json.deserializationError
* @param id the document id, required not null
*/
protected[core] class DocId private (val id: String) extends AnyVal {
- def asString = id // to make explicit that this is a string conversion
- protected[core] def asDocInfo = DocInfo(this)
- protected[core] def asDocInfo(rev: DocRevision) = DocInfo(this, rev)
- protected[entity] def toJson = JsString(id)
- override def toString = id
+ def asString = id // to make explicit that this is a string conversion
+ protected[core] def asDocInfo = DocInfo(this)
+ protected[core] def asDocInfo(rev: DocRevision) = DocInfo(this, rev)
+ protected[entity] def toJson = JsString(id)
+ override def toString = id
}
/**
@@ -53,9 +53,9 @@ protected[core] class DocId private (val id: String) extends AnyVal {
* @param rev the document revision, optional
*/
protected[core] class DocRevision private (val rev: String) extends AnyVal {
- def asString = rev // to make explicit that this is a string conversion
- def empty = rev == null
- override def toString = rev
+ def asString = rev // to make explicit that this is a string conversion
+ def empty = rev == null
+ override def toString = rev
}
/**
@@ -68,79 +68,83 @@ protected[core] class DocRevision private (val rev: String) extends AnyVal {
* @param rev the document revision, optional; this is an opaque value determined by the datastore
*/
protected[core] case class DocInfo protected[entity] (id: DocId, rev: DocRevision = DocRevision.empty) {
- override def toString = {
- if (rev.empty) {
- s"id: $id"
- } else {
- s"id: $id, rev: $rev"
- }
+ override def toString = {
+ if (rev.empty) {
+ s"id: $id"
+ } else {
+ s"id: $id, rev: $rev"
}
+ }
- override def hashCode = {
- if (rev.empty) {
- id.hashCode
- } else {
- s"$id.$rev".hashCode
- }
+ override def hashCode = {
+ if (rev.empty) {
+ id.hashCode
+ } else {
+ s"$id.$rev".hashCode
}
+ }
}
protected[core] object DocId extends ArgNormalizer[DocId] {
- /**
- * Unapply method for convenience of case matching.
- */
- def unapply(s: String): Option[DocId] = Try(DocId(s)).toOption
-
- implicit val serdes = new RootJsonFormat[DocId] {
- def write(d: DocId) = d.toJson
-
- def read(value: JsValue) = Try {
- val JsString(s) = value
- new DocId(s)
- } getOrElse deserializationError("doc id malformed")
- }
+
+ /**
+ * Unapply method for convenience of case matching.
+ */
+ def unapply(s: String): Option[DocId] = Try(DocId(s)).toOption
+
+ implicit val serdes = new RootJsonFormat[DocId] {
+ def write(d: DocId) = d.toJson
+
+ def read(value: JsValue) =
+ Try {
+ val JsString(s) = value
+ new DocId(s)
+ } getOrElse deserializationError("doc id malformed")
+ }
}
protected[core] object DocRevision {
- /**
- * Creates a DocRevision. Normalizes the revision if necessary.
- *
- * @param s is the document revision as a string, may be null
- * @return DocRevision
- */
- protected[core] def apply(s: String): DocRevision = new DocRevision(trim(s))
-
- protected[core] val empty: DocRevision = new DocRevision(null)
-
- implicit val serdes = new RootJsonFormat[DocRevision] {
- def write(d: DocRevision) = if (d.rev != null) JsString(d.rev) else JsNull
-
- def read(value: JsValue) = value match {
- case JsString(s) => DocRevision(s)
- case JsNull => DocRevision.empty
- case _ => deserializationError("doc revision malformed")
- }
+
+ /**
+ * Creates a DocRevision. Normalizes the revision if necessary.
+ *
+ * @param s is the document revision as a string, may be null
+ * @return DocRevision
+ */
+ protected[core] def apply(s: String): DocRevision = new DocRevision(trim(s))
+
+ protected[core] val empty: DocRevision = new DocRevision(null)
+
+ implicit val serdes = new RootJsonFormat[DocRevision] {
+ def write(d: DocRevision) = if (d.rev != null) JsString(d.rev) else JsNull
+
+ def read(value: JsValue) = value match {
+ case JsString(s) => DocRevision(s)
+ case JsNull => DocRevision.empty
+ case _ => deserializationError("doc revision malformed")
}
+ }
}
protected[core] object DocInfo {
- /**
- * Creates a DocInfo with id set to the argument and no revision.
- *
- * @param id is the document identifier, must be defined
- * @throws IllegalArgumentException if id is null or empty
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(id: String): DocInfo = DocInfo(DocId(id))
-
- /**
- * Creates a DocInfo with id and revision per the provided arguments.
- *
- * @param id is the document identifier, must be defined
- * @param rev the document revision, optional
- * @return DocInfo for id and revision
- * @throws IllegalArgumentException if id is null or empty
- */
- @throws[IllegalArgumentException]
- protected[core] def !(id: String, rev: String): DocInfo = DocInfo(DocId(id), DocRevision(rev))
+
+ /**
+ * Creates a DocInfo with id set to the argument and no revision.
+ *
+ * @param id is the document identifier, must be defined
+ * @throws IllegalArgumentException if id is null or empty
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(id: String): DocInfo = DocInfo(DocId(id))
+
+ /**
+ * Creates a DocInfo with id and revision per the provided arguments.
+ *
+ * @param id is the document identifier, must be defined
+ * @param rev the document revision, optional
+ * @return DocInfo for id and revision
+ * @throws IllegalArgumentException if id is null or empty
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def !(id: String, rev: String): DocInfo = DocInfo(DocId(id), DocRevision(rev))
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/EntityPath.scala b/common/scala/src/main/scala/whisk/core/entity/EntityPath.scala
index 281d3f1..26e1fb1 100644
--- a/common/scala/src/main/scala/whisk/core/entity/EntityPath.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/EntityPath.scala
@@ -40,125 +40,130 @@ import whisk.http.Messages
* @param path the sequence of parts that make up a namespace path
*/
protected[core] class EntityPath private (private val path: Seq[String]) extends AnyVal {
- def namespace: String = path.foldLeft("")((a, b) => if (a != "") a.trim + EntityPath.PATHSEP + b.trim else b.trim)
-
- /**
- * Adds segment to path.
- */
- def addPath(e: EntityName) = EntityPath(path :+ e.name)
-
- /**
- * Concatenates given path to existin path.
- */
- def addPath(p: EntityPath) = EntityPath(path ++ p.path)
-
- /**
- * Computes the relative path by dropping the leftmost segment. The return is an option
- * since dropping a singleton results in an invalid path.
- */
- def relativePath: Option[EntityPath] = Try(EntityPath(path.drop(1))).toOption
-
- /**
- * @return the root of the path (the first segment).
- */
- def root: EntityName = EntityName(path.head)
-
- /**
- * @return the last segment of the path.
- */
- def last: EntityName = EntityName(path.last)
-
- /**
- * @return true iff the path contains exactly one segment (i.e., the namespace)
- */
- def defaultPackage: Boolean = path.size == 1
-
- /**
- * Replaces root of this path with given namespace iff the root is
- * the default namespace.
- */
- def resolveNamespace(newNamespace: EntityName): EntityPath = {
- // check if namespace is default
- if (root.toPath == EntityPath.DEFAULT) {
- val newPath = path.updated(0, newNamespace.name)
- EntityPath(newPath)
- } else this
- }
-
- /**
- * Converts the path to a fully qualified name. The path must contains at least 2 parts.
- *
- * @throws IllegalArgumentException if the path does not conform to schema (at least namespace and entity name must be present0
- */
- @throws[IllegalArgumentException]
- def toFullyQualifiedEntityName = {
- require(path.size > 1, Messages.malformedFullyQualifiedEntityName)
- val name = last
- val newPath = EntityPath(path.dropRight(1))
- FullyQualifiedEntityName(newPath, name)
- }
-
- def toDocId = DocId(namespace)
- def toJson = JsString(namespace)
- def asString = namespace // to make explicit that this is a string conversion
- override def toString = namespace
+ def namespace: String = path.foldLeft("")((a, b) => if (a != "") a.trim + EntityPath.PATHSEP + b.trim else b.trim)
+
+ /**
+ * Adds segment to path.
+ */
+ def addPath(e: EntityName) = EntityPath(path :+ e.name)
+
+ /**
+ * Concatenates given path to existin path.
+ */
+ def addPath(p: EntityPath) = EntityPath(path ++ p.path)
+
+ /**
+ * Computes the relative path by dropping the leftmost segment. The return is an option
+ * since dropping a singleton results in an invalid path.
+ */
+ def relativePath: Option[EntityPath] = Try(EntityPath(path.drop(1))).toOption
+
+ /**
+ * @return the root of the path (the first segment).
+ */
+ def root: EntityName = EntityName(path.head)
+
+ /**
+ * @return the last segment of the path.
+ */
+ def last: EntityName = EntityName(path.last)
+
+ /**
+ * @return true iff the path contains exactly one segment (i.e., the namespace)
+ */
+ def defaultPackage: Boolean = path.size == 1
+
+ /**
+ * Replaces root of this path with given namespace iff the root is
+ * the default namespace.
+ */
+ def resolveNamespace(newNamespace: EntityName): EntityPath = {
+ // check if namespace is default
+ if (root.toPath == EntityPath.DEFAULT) {
+ val newPath = path.updated(0, newNamespace.name)
+ EntityPath(newPath)
+ } else this
+ }
+
+ /**
+ * Converts the path to a fully qualified name. The path must contains at least 2 parts.
+ *
+ * @throws IllegalArgumentException if the path does not conform to schema (at least namespace and entity name must be present0
+ */
+ @throws[IllegalArgumentException]
+ def toFullyQualifiedEntityName = {
+ require(path.size > 1, Messages.malformedFullyQualifiedEntityName)
+ val name = last
+ val newPath = EntityPath(path.dropRight(1))
+ FullyQualifiedEntityName(newPath, name)
+ }
+
+ def toDocId = DocId(namespace)
+ def toJson = JsString(namespace)
+ def asString = namespace // to make explicit that this is a string conversion
+ override def toString = namespace
}
protected[core] object EntityPath {
- /** Path separator */
- protected[core] val PATHSEP = "/"
-
- /**
- * Default namespace name. This name is not a valid entity name and is a special string
- * that allows omission of the namespace during API calls. It is only used in the URI
- * namespace extraction.
- */
- protected[core] val DEFAULT = EntityPath("_")
-
- /**
- * Constructs a Namespace from a string. String must be a valid path, consisting of
- * a valid EntityName separated by the Namespace separator character.
- *
- * @param path a valid namespace path
- * @return Namespace for the path
- * @throws IllegalArgumentException if the path does not conform to schema
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(path: String): EntityPath = {
- require(path != null, "path undefined")
- val parts = path.split(PATHSEP).filter { _.nonEmpty }.toSeq
- EntityPath(parts)
- }
-
- /**
- * Namespace is a path string of allowed characters. The path consists of parts each of which
- * must be a valid EntityName, separated by the Namespace separator character. The constructor
- * accepts a sequence of path parts and can reconstruct the path from it.
- *
- * @param path the sequence of parts that make up a namespace path
- * @throws IllegalArgumentException if any of the parts are not valid path part names
- */
- @throws[IllegalArgumentException]
- private def apply(parts: Seq[String]): EntityPath = {
- require(parts != null && parts.nonEmpty, "path undefined")
- require(parts.forall { s => s != null && EntityName.entityNameMatcher(s).matches }, s"path contains invalid parts ${parts.toString}")
- new EntityPath(parts)
- }
-
- /** Returns true iff the path is a valid namespace path. */
- protected[core] def validate(path: String): Boolean = {
- Try { EntityPath(path) } map { _ => true } getOrElse false
- }
-
- implicit val serdes = new RootJsonFormat[EntityPath] {
- def write(n: EntityPath) = n.toJson
-
- def read(value: JsValue) = Try {
- val JsString(name) = value
- EntityPath(name)
- } getOrElse deserializationError("namespace malformed")
- }
+ /** Path separator */
+ protected[core] val PATHSEP = "/"
+
+ /**
+ * Default namespace name. This name is not a valid entity name and is a special string
+ * that allows omission of the namespace during API calls. It is only used in the URI
+ * namespace extraction.
+ */
+ protected[core] val DEFAULT = EntityPath("_")
+
+ /**
+ * Constructs a Namespace from a string. String must be a valid path, consisting of
+ * a valid EntityName separated by the Namespace separator character.
+ *
+ * @param path a valid namespace path
+ * @return Namespace for the path
+ * @throws IllegalArgumentException if the path does not conform to schema
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(path: String): EntityPath = {
+ require(path != null, "path undefined")
+ val parts = path.split(PATHSEP).filter { _.nonEmpty }.toSeq
+ EntityPath(parts)
+ }
+
+ /**
+ * Namespace is a path string of allowed characters. The path consists of parts each of which
+ * must be a valid EntityName, separated by the Namespace separator character. The constructor
+ * accepts a sequence of path parts and can reconstruct the path from it.
+ *
+ * @param path the sequence of parts that make up a namespace path
+ * @throws IllegalArgumentException if any of the parts are not valid path part names
+ */
+ @throws[IllegalArgumentException]
+ private def apply(parts: Seq[String]): EntityPath = {
+ require(parts != null && parts.nonEmpty, "path undefined")
+ require(parts.forall { s =>
+ s != null && EntityName.entityNameMatcher(s).matches
+ }, s"path contains invalid parts ${parts.toString}")
+ new EntityPath(parts)
+ }
+
+ /** Returns true iff the path is a valid namespace path. */
+ protected[core] def validate(path: String): Boolean = {
+ Try { EntityPath(path) } map { _ =>
+ true
+ } getOrElse false
+ }
+
+ implicit val serdes = new RootJsonFormat[EntityPath] {
+ def write(n: EntityPath) = n.toJson
+
+ def read(value: JsValue) =
+ Try {
+ val JsString(name) = value
+ EntityPath(name)
+ } getOrElse deserializationError("namespace malformed")
+ }
}
/**
@@ -169,49 +174,50 @@ protected[core] object EntityPath {
* before creating a new instance.
*/
protected[core] class EntityName private (val name: String) extends AnyVal {
- def asString = name // to make explicit that this is a string conversion
- def toJson = JsString(name)
- def toPath: EntityPath = EntityPath(name)
- def addPath(e: EntityName): EntityPath = toPath.addPath(e)
- def addPath(e: Option[EntityName]): EntityPath = e map { toPath.addPath(_) } getOrElse toPath
- override def toString = name
+ def asString = name // to make explicit that this is a string conversion
+ def toJson = JsString(name)
+ def toPath: EntityPath = EntityPath(name)
+ def addPath(e: EntityName): EntityPath = toPath.addPath(e)
+ def addPath(e: Option[EntityName]): EntityPath = e map { toPath.addPath(_) } getOrElse toPath
+ override def toString = name
}
protected[core] object EntityName {
- protected[core] val ENTITY_NAME_MAX_LENGTH = 256
-
- /**
- * Allowed path part or entity name format (excludes path separator): first character
- * is a letter|digit|underscore, followed by one or more allowed characters in [\w@ .-].
- * The name may not have trailing white space.
- */
- protected[core] val REGEX = raw"\A([\w]|[\w][\w@ .-]{0,${ENTITY_NAME_MAX_LENGTH - 2}}[\w@.-])\z"
- private val entityNamePattern = REGEX.r.pattern // compile once
- protected[core] def entityNameMatcher(s: String): Matcher = entityNamePattern.matcher(s)
-
- /**
- * Unapply method for convenience of case matching.
- */
- protected[core] def unapply(name: String): Option[EntityName] = Try(EntityName(name)).toOption
-
- /**
- * EntityName is a string of allowed characters.
- *
- * @param name the entity name
- * @throws IllegalArgumentException if the name does not conform to schema
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(name: String): EntityName = {
- require(name != null && entityNameMatcher(name).matches, s"name [$name] is not allowed")
- new EntityName(name)
- }
-
- implicit val serdes = new RootJsonFormat[EntityName] {
- def write(n: EntityName) = n.toJson
-
- def read(value: JsValue) = Try {
- val JsString(name) = value
- EntityName(name)
- } getOrElse deserializationError("entity name malformed")
- }
+ protected[core] val ENTITY_NAME_MAX_LENGTH = 256
+
+ /**
+ * Allowed path part or entity name format (excludes path separator): first character
+ * is a letter|digit|underscore, followed by one or more allowed characters in [\w@ .-].
+ * The name may not have trailing white space.
+ */
+ protected[core] val REGEX = raw"\A([\w]|[\w][\w@ .-]{0,${ENTITY_NAME_MAX_LENGTH - 2}}[\w@.-])\z"
+ private val entityNamePattern = REGEX.r.pattern // compile once
+ protected[core] def entityNameMatcher(s: String): Matcher = entityNamePattern.matcher(s)
+
+ /**
+ * Unapply method for convenience of case matching.
+ */
+ protected[core] def unapply(name: String): Option[EntityName] = Try(EntityName(name)).toOption
+
+ /**
+ * EntityName is a string of allowed characters.
+ *
+ * @param name the entity name
+ * @throws IllegalArgumentException if the name does not conform to schema
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(name: String): EntityName = {
+ require(name != null && entityNameMatcher(name).matches, s"name [$name] is not allowed")
+ new EntityName(name)
+ }
+
+ implicit val serdes = new RootJsonFormat[EntityName] {
+ def write(n: EntityName) = n.toJson
+
+ def read(value: JsValue) =
+ Try {
+ val JsString(name) = value
+ EntityName(name)
+ } getOrElse deserializationError("entity name malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Exec.scala b/common/scala/src/main/scala/whisk/core/entity/Exec.scala
index 4b6fe2a..49ede1d 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Exec.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Exec.scala
@@ -45,12 +45,13 @@ import whisk.core.entity.size.SizeString
* main : name of the entry point function, when using a non-default value (for Java, the name of the main class)" }
*/
sealed abstract class Exec extends ByteSizeable {
- override def toString = Exec.serdes.write(this).compactPrint
- /** A type descriptor. */
- val kind: String
+ override def toString = Exec.serdes.write(this).compactPrint
- /** When true exec may not be executed or updated. */
- val deprecated: Boolean
+ /** A type descriptor. */
+ val kind: String
+
+ /** When true exec may not be executed or updated. */
+ val deprecated: Boolean
}
/**
@@ -58,215 +59,222 @@ sealed abstract class Exec extends ByteSizeable {
* code explicitly (i.e., any action other than a sequence).
*/
sealed abstract class CodeExec[+T <% SizeConversion] extends Exec {
- /** An entrypoint (typically name of 'main' function). 'None' means a default value will be used. */
- val entryPoint: Option[String]
- /** The executable code. */
- val code: T
+ /** An entrypoint (typically name of 'main' function). 'None' means a default value will be used. */
+ val entryPoint: Option[String]
+
+ /** The executable code. */
+ val code: T
- /** Serialize code to a JSON value. */
- def codeAsJson: JsValue
+ /** Serialize code to a JSON value. */
+ def codeAsJson: JsValue
- /** The runtime image (either built-in or a public image). */
- val image: ImageName
+ /** The runtime image (either built-in or a public image). */
+ val image: ImageName
- /** Indicates if the action execution generates log markers to stdout/stderr once action activation completes. */
- val sentinelledLogs: Boolean
+ /** Indicates if the action execution generates log markers to stdout/stderr once action activation completes. */
+ val sentinelledLogs: Boolean
- /** Indicates if a container image is required from the registry to execute the action. */
- val pull: Boolean
+ /** Indicates if a container image is required from the registry to execute the action. */
+ val pull: Boolean
- /**
- * Indicates whether the code is stored in a text-readable or binary format.
- * The binary bit may be read from the database but currently it is always computed
- * when the "code" is moved to an attachment this may get changed to avoid recomputing
- * the binary property.
- */
- val binary: Boolean
+ /**
+ * Indicates whether the code is stored in a text-readable or binary format.
+ * The binary bit may be read from the database but currently it is always computed
+ * when the "code" is moved to an attachment this may get changed to avoid recomputing
+ * the binary property.
+ */
+ val binary: Boolean
- override def size = code.sizeInBytes + entryPoint.map(_.sizeInBytes).getOrElse(0.B)
+ override def size = code.sizeInBytes + entryPoint.map(_.sizeInBytes).getOrElse(0.B)
}
-protected[core] case class CodeExecAsString(
- manifest: RuntimeManifest,
- override val code: String,
- override val entryPoint: Option[String])
+protected[core] case class CodeExecAsString(manifest: RuntimeManifest,
+ override val code: String,
+ override val entryPoint: Option[String])
extends CodeExec[String] {
- override val kind = manifest.kind
- override val image = manifest.image
- override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
- override val deprecated = manifest.deprecated.getOrElse(false)
- override val pull = false
- override lazy val binary = Exec.isBinaryCode(code)
- override def codeAsJson = JsString(code)
+ override val kind = manifest.kind
+ override val image = manifest.image
+ override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
+ override val deprecated = manifest.deprecated.getOrElse(false)
+ override val pull = false
+ override lazy val binary = Exec.isBinaryCode(code)
+ override def codeAsJson = JsString(code)
}
-protected[core] case class CodeExecAsAttachment(
- manifest: RuntimeManifest,
- override val code: Attachment[String],
- override val entryPoint: Option[String])
+protected[core] case class CodeExecAsAttachment(manifest: RuntimeManifest,
+ override val code: Attachment[String],
+ override val entryPoint: Option[String])
extends CodeExec[Attachment[String]] {
- override val kind = manifest.kind
- override val image = manifest.image
- override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
- override val deprecated = manifest.deprecated.getOrElse(false)
- override val pull = false
- override lazy val binary = true
- override def codeAsJson = code.toJson
-
- def inline(bytes: Array[Byte]): CodeExecAsAttachment = {
- val encoded = new String(bytes, StandardCharsets.UTF_8)
- copy(code = Inline(encoded))
- }
-
- def attach: CodeExecAsAttachment = {
- manifest.attached.map { a =>
- copy(code = Attached(a.attachmentName, a.attachmentType))
- } getOrElse this
- }
+ override val kind = manifest.kind
+ override val image = manifest.image
+ override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
+ override val deprecated = manifest.deprecated.getOrElse(false)
+ override val pull = false
+ override lazy val binary = true
+ override def codeAsJson = code.toJson
+
+ def inline(bytes: Array[Byte]): CodeExecAsAttachment = {
+ val encoded = new String(bytes, StandardCharsets.UTF_8)
+ copy(code = Inline(encoded))
+ }
+
+ def attach: CodeExecAsAttachment = {
+ manifest.attached.map { a =>
+ copy(code = Attached(a.attachmentName, a.attachmentType))
+ } getOrElse this
+ }
}
/**
* @param image the image name
* @param code an optional script or zip archive (as base64 encoded) string
*/
-protected[core] case class BlackBoxExec(
- override val image: ImageName,
- override val code: Option[String],
- override val entryPoint: Option[String],
- val native: Boolean)
+protected[core] case class BlackBoxExec(override val image: ImageName,
+ override val code: Option[String],
+ override val entryPoint: Option[String],
+ val native: Boolean)
extends CodeExec[Option[String]] {
- override val kind = Exec.BLACKBOX
- override val deprecated = false
- override def codeAsJson = code.toJson
- override lazy val binary = code map { Exec.isBinaryCode(_) } getOrElse false
- override val sentinelledLogs = native
- override val pull = !native
- override def size = super.size + image.publicImageName.sizeInBytes
+ override val kind = Exec.BLACKBOX
+ override val deprecated = false
+ override def codeAsJson = code.toJson
+ override lazy val binary = code map { Exec.isBinaryCode(_) } getOrElse false
+ override val sentinelledLogs = native
+ override val pull = !native
+ override def size = super.size + image.publicImageName.sizeInBytes
}
protected[core] case class SequenceExec(components: Vector[FullyQualifiedEntityName]) extends Exec {
- override val kind = Exec.SEQUENCE
- override val deprecated = false
- override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)
+ override val kind = Exec.SEQUENCE
+ override val deprecated = false
+ override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)
}
-protected[core] object Exec
- extends ArgNormalizer[Exec]
- with DefaultJsonProtocol {
-
- val sizeLimit = 48 MB
-
- // The possible values of the JSON 'kind' field for certain runtimes:
- // - Sequence because it is an intrinsic
- // - Black Box because it is a type marker
- protected[core] val SEQUENCE = "sequence"
- protected[core] val BLACKBOX = "blackbox"
+protected[core] object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol {
- private def execManifests = ExecManifest.runtimesManifest
+ val sizeLimit = 48 MB
- override protected[core] implicit lazy val serdes = new RootJsonFormat[Exec] {
- private def attFmt[T: JsonFormat] = Attachments.serdes[T]
- private lazy val runtimes: Set[String] = execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)
+ // The possible values of the JSON 'kind' field for certain runtimes:
+ // - Sequence because it is an intrinsic
+ // - Black Box because it is a type marker
+ protected[core] val SEQUENCE = "sequence"
+ protected[core] val BLACKBOX = "blackbox"
- override def write(e: Exec) = e match {
- case c: CodeExecAsString =>
- val base = Map("kind" -> JsString(c.kind), "code" -> JsString(c.code), "binary" -> JsBoolean(c.binary))
- val main = c.entryPoint.map("main" -> JsString(_))
- JsObject(base ++ main)
+ private def execManifests = ExecManifest.runtimesManifest
- case a: CodeExecAsAttachment =>
- val base = Map("kind" -> JsString(a.kind), "code" -> attFmt[String].write(a.code), "binary" -> JsBoolean(a.binary))
- val main = a.entryPoint.map("main" -> JsString(_))
- JsObject(base ++ main)
+ override protected[core] implicit lazy val serdes = new RootJsonFormat[Exec] {
+ private def attFmt[T: JsonFormat] = Attachments.serdes[T]
+ private lazy val runtimes: Set[String] = execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)
- case s @ SequenceExec(comp) =>
- JsObject("kind" -> JsString(s.kind), "components" -> comp.map(_.qualifiedNameWithLeadingSlash).toJson)
+ override def write(e: Exec) = e match {
+ case c: CodeExecAsString =>
+ val base = Map("kind" -> JsString(c.kind), "code" -> JsString(c.code), "binary" -> JsBoolean(c.binary))
+ val main = c.entryPoint.map("main" -> JsString(_))
+ JsObject(base ++ main)
- case b: BlackBoxExec =>
- val base = Map("kind" -> JsString(b.kind), "image" -> JsString(b.image.publicImageName), "binary" -> JsBoolean(b.binary))
- val code = b.code.filter(_.trim.nonEmpty).map("code" -> JsString(_))
- val main = b.entryPoint.map("main" -> JsString(_))
- JsObject(base ++ code ++ main)
- }
+ case a: CodeExecAsAttachment =>
+ val base =
+ Map("kind" -> JsString(a.kind), "code" -> attFmt[String].write(a.code), "binary" -> JsBoolean(a.binary))
+ val main = a.entryPoint.map("main" -> JsString(_))
+ JsObject(base ++ main)
- override def read(v: JsValue) = {
- require(v != null)
+ case s @ SequenceExec(comp) =>
+ JsObject("kind" -> JsString(s.kind), "components" -> comp.map(_.qualifiedNameWithLeadingSlash).toJson)
- val obj = v.asJsObject
-
- val kind = obj.fields.get("kind") match {
- case Some(JsString(k)) => k.trim.toLowerCase
- case _ => throw new DeserializationException("'kind' must be a string defined in 'exec'")
- }
+ case b: BlackBoxExec =>
+ val base =
+ Map("kind" -> JsString(b.kind), "image" -> JsString(b.image.publicImageName), "binary" -> JsBoolean(b.binary))
+ val code = b.code.filter(_.trim.nonEmpty).map("code" -> JsString(_))
+ val main = b.entryPoint.map("main" -> JsString(_))
+ JsObject(base ++ code ++ main)
+ }
- lazy val optMainField: Option[String] = obj.fields.get("main") match {
- case Some(JsString(m)) => Some(m)
- case Some(_) => throw new DeserializationException(s"if defined, 'main' be a string in 'exec' for '$kind' actions")
- case None => None
+ override def read(v: JsValue) = {
+ require(v != null)
+
+ val obj = v.asJsObject
+
+ val kind = obj.fields.get("kind") match {
+ case Some(JsString(k)) => k.trim.toLowerCase
+ case _ => throw new DeserializationException("'kind' must be a string defined in 'exec'")
+ }
+
+ lazy val optMainField: Option[String] = obj.fields.get("main") match {
+ case Some(JsString(m)) => Some(m)
+ case Some(_) =>
+ throw new DeserializationException(s"if defined, 'main' be a string in 'exec' for '$kind' actions")
+ case None => None
+ }
+
+ kind match {
+ case Exec.SEQUENCE =>
+ val comp: Vector[FullyQualifiedEntityName] = obj.fields.get("components") match {
+ case Some(JsArray(components)) => components map (FullyQualifiedEntityName.serdes.read(_))
+ case Some(_) => throw new DeserializationException(s"'components' must be an array")
+ case None => throw new DeserializationException(s"'components' must be defined for sequence kind")
+ }
+ SequenceExec(comp)
+
+ case Exec.BLACKBOX =>
+ val image: ImageName = obj.fields.get("image") match {
+ case Some(JsString(i)) => ImageName.fromString(i).get // throws deserialization exception on failure
+ case _ =>
+ throw new DeserializationException(
+ s"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
+ }
+ val code: Option[String] = obj.fields.get("code") match {
+ case Some(JsString(i)) => if (i.trim.nonEmpty) Some(i) else None
+ case Some(_) =>
+ throw new DeserializationException(
+ s"if defined, 'code' must a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
+ case None => None
+ }
+ val native = execManifests.blackboxImages.contains(image)
+ BlackBoxExec(image, code, optMainField, native)
+
+ case _ =>
+ // map "default" virtual runtime versions to the currently blessed actual runtime version
+ val manifest = execManifests.resolveDefaultRuntime(kind) match {
+ case Some(k) => k
+ case None => throw new DeserializationException(s"kind '$kind' not in $runtimes")
+ }
+
+ manifest.attached
+ .map { a =>
+ val jar: Attachment[String] = {
+ // java actions once stored the attachment in "jar" instead of "code"
+ obj.fields.get("code").orElse(obj.fields.get("jar"))
+ } map {
+ attFmt[String].read(_)
+ } getOrElse {
+ throw new DeserializationException(
+ s"'code' must be a valid base64 string in 'exec' for '$kind' actions")
+ }
+ val main = optMainField.orElse {
+ if (manifest.requireMain.exists(identity)) {
+ throw new DeserializationException(s"'main' must be a string defined in 'exec' for '$kind' actions")
+ } else None
+ }
+ CodeExecAsAttachment(manifest, jar, main)
}
-
- kind match {
- case Exec.SEQUENCE =>
- val comp: Vector[FullyQualifiedEntityName] = obj.fields.get("components") match {
- case Some(JsArray(components)) => components map (FullyQualifiedEntityName.serdes.read(_))
- case Some(_) => throw new DeserializationException(s"'components' must be an array")
- case None => throw new DeserializationException(s"'components' must be defined for sequence kind")
- }
- SequenceExec(comp)
-
- case Exec.BLACKBOX =>
- val image: ImageName = obj.fields.get("image") match {
- case Some(JsString(i)) => ImageName.fromString(i).get // throws deserialization exception on failure
- case _ => throw new DeserializationException(s"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
- }
- val code: Option[String] = obj.fields.get("code") match {
- case Some(JsString(i)) => if (i.trim.nonEmpty) Some(i) else None
- case Some(_) => throw new DeserializationException(s"if defined, 'code' must a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
- case None => None
- }
- val native = execManifests.blackboxImages.contains(image)
- BlackBoxExec(image, code, optMainField, native)
-
+ .getOrElse {
+ val code: String = obj.fields.get("code") match {
+ case Some(JsString(c)) => c
case _ =>
- // map "default" virtual runtime versions to the currently blessed actual runtime version
- val manifest = execManifests.resolveDefaultRuntime(kind) match {
- case Some(k) => k
- case None => throw new DeserializationException(s"kind '$kind' not in $runtimes")
- }
-
- manifest.attached.map { a =>
- val jar: Attachment[String] = {
- // java actions once stored the attachment in "jar" instead of "code"
- obj.fields.get("code").orElse(obj.fields.get("jar"))
- } map {
- attFmt[String].read(_)
- } getOrElse {
- throw new DeserializationException(s"'code' must be a valid base64 string in 'exec' for '$kind' actions")
- }
- val main = optMainField.orElse {
- if (manifest.requireMain.exists(identity)) {
- throw new DeserializationException(s"'main' must be a string defined in 'exec' for '$kind' actions")
- } else None
- }
- CodeExecAsAttachment(manifest, jar, main)
- }.getOrElse {
- val code: String = obj.fields.get("code") match {
- case Some(JsString(c)) => c
- case _ => throw new DeserializationException(s"'code' must be a string defined in 'exec' for '$kind' actions")
- }
- CodeExecAsString(manifest, code, optMainField)
- }
+ throw new DeserializationException(s"'code' must be a string defined in 'exec' for '$kind' actions")
+ }
+ CodeExecAsString(manifest, code, optMainField)
}
- }
+ }
}
+ }
- val isBase64Pattern = new Regex("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$").pattern
+ val isBase64Pattern = new Regex("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$").pattern
- def isBinaryCode(code: String): Boolean = {
- if (code != null) {
- val t = code.trim
- (t.length > 0) && (t.length % 4 == 0) && isBase64Pattern.matcher(t).matches()
- } else false
- }
+ def isBinaryCode(code: String): Boolean = {
+ if (code != null) {
+ val t = code.trim
+ (t.length > 0) && (t.length % 4 == 0) && isBase64Pattern.matcher(t).matches()
+ } else false
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala b/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala
index d44029b..479f23d 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala
@@ -31,227 +31,234 @@ import whisk.core.entity.Attachments.Attached._
*/
protected[core] object ExecManifest {
- /**
- * Required properties to initialize this singleton via WhiskConfig.
- */
- protected[core] def requiredProperties = Map(WhiskConfig.runtimesManifest -> null)
+ /**
+ * Required properties to initialize this singleton via WhiskConfig.
+ */
+ protected[core] def requiredProperties = Map(WhiskConfig.runtimesManifest -> null)
- /**
- * Reads runtimes manifest from WhiskConfig and initializes the
- * singleton Runtime instance.
- *
- * @param config a valid configuration
- * @param reinit re-initialize singleton iff true
- * @return the manifest if initialized successfully, or if previously initialized
- */
- protected[core] def initialize(config: WhiskConfig, reinit: Boolean = false): Try[Runtimes] = {
- if (manifest.isEmpty || reinit) {
- val mf = Try(config.runtimesManifest.parseJson.asJsObject).flatMap(runtimes(_))
- mf.foreach(m => manifest = Some(m))
- mf
- } else Success(manifest.get)
+ /**
+ * Reads runtimes manifest from WhiskConfig and initializes the
+ * singleton Runtime instance.
+ *
+ * @param config a valid configuration
+ * @param reinit re-initialize singleton iff true
+ * @return the manifest if initialized successfully, or if previously initialized
+ */
+ protected[core] def initialize(config: WhiskConfig, reinit: Boolean = false): Try[Runtimes] = {
+ if (manifest.isEmpty || reinit) {
+ val mf = Try(config.runtimesManifest.parseJson.asJsObject).flatMap(runtimes(_))
+ mf.foreach(m => manifest = Some(m))
+ mf
+ } else Success(manifest.get)
+ }
+
+ /**
+ * Gets existing runtime manifests.
+ *
+ * @return singleton Runtimes instance previous initialized from WhiskConfig
+ * @throws IllegalStateException if singleton was not previously initialized
+ */
+ @throws[IllegalStateException]
+ protected[core] def runtimesManifest: Runtimes = {
+ manifest.getOrElse {
+ throw new IllegalStateException("Runtimes manifest is not initialized.")
}
+ }
- /**
- * Gets existing runtime manifests.
- *
- * @return singleton Runtimes instance previous initialized from WhiskConfig
- * @throws IllegalStateException if singleton was not previously initialized
- */
- @throws[IllegalStateException]
- protected[core] def runtimesManifest: Runtimes = {
- manifest.getOrElse {
- throw new IllegalStateException("Runtimes manifest is not initialized.")
- }
+ private var manifest: Option[Runtimes] = None
+
+ /**
+ * @param config a configuration object as JSON
+ * @return Runtimes instance
+ */
+ protected[entity] def runtimes(config: JsObject): Try[Runtimes] = Try {
+ val prefix = config.fields.get("defaultImagePrefix").map(_.convertTo[String])
+ val tag = config.fields.get("defaultImageTag").map(_.convertTo[String])
+ val runtimes = config
+ .fields("runtimes")
+ .convertTo[Map[String, Set[RuntimeManifest]]]
+ .map {
+ case (name, versions) =>
+ RuntimeFamily(name, versions.map { mf =>
+ val img = ImageName(mf.image.name, mf.image.prefix.orElse(prefix), mf.image.tag.orElse(tag))
+ mf.copy(image = img)
+ })
+ }
+ .toSet
+ val blackbox = config.fields
+ .get("blackboxes")
+ .map(_.convertTo[Set[ImageName]].map { image =>
+ ImageName(image.name, image.prefix.orElse(prefix), image.tag.orElse(tag))
+ })
+ Runtimes(runtimes, blackbox.getOrElse(Set.empty))
+ }
+
+ /**
+ * A runtime manifest describes the "exec" runtime support.
+ *
+ * @param kind the name of the kind e.g., nodejs:6
+ * @param deprecated true iff the runtime is deprecated (allows get/delete but not create/update/invoke)
+ * @param default true iff the runtime is the default kind for its family (nodejs:default -> nodejs:6)
+ * @param attached true iff the source is an attachments (not inlined source)
+ * @param requireMain true iff main entry point is not optional
+ * @param sentinelledLogs true iff the runtime generates stdout/stderr log sentinels after an activation
+ * @param image optional image name, otherwise inferred via fixed mapping (remove colons and append 'action')
+ */
+ protected[core] case class RuntimeManifest(kind: String,
+ image: ImageName,
+ deprecated: Option[Boolean] = None,
+ default: Option[Boolean] = None,
+ attached: Option[Attached] = None,
+ requireMain: Option[Boolean] = None,
+ sentinelledLogs: Option[Boolean] = None) {
+
+ protected[entity] def toJsonSummary = {
+ JsObject(
+ "kind" -> kind.toJson,
+ "image" -> image.publicImageName.toJson,
+ "deprecated" -> deprecated.getOrElse(false).toJson,
+ "default" -> default.getOrElse(false).toJson,
+ "attached" -> attached.isDefined.toJson,
+ "requireMain" -> requireMain.getOrElse(false).toJson)
}
+ }
- private var manifest: Option[Runtimes] = None
+ /**
+ * An image name for an action refers to the container image canonically as
+ * "prefix/name[:tag]" e.g., "openwhisk/python3action:latest".
+ */
+ protected[core] case class ImageName(name: String, prefix: Option[String] = None, tag: Option[String] = None) {
/**
- * @param config a configuration object as JSON
- * @return Runtimes instance
+ * The name of the public image for an action kind.
*/
- protected[entity] def runtimes(config: JsObject): Try[Runtimes] = Try {
- val prefix = config.fields.get("defaultImagePrefix").map(_.convertTo[String])
- val tag = config.fields.get("defaultImageTag").map(_.convertTo[String])
- val runtimes = config.fields("runtimes")
- .convertTo[Map[String, Set[RuntimeManifest]]]
- .map {
- case (name, versions) =>
- RuntimeFamily(name, versions.map { mf =>
- val img = ImageName(mf.image.name, mf.image.prefix.orElse(prefix), mf.image.tag.orElse(tag))
- mf.copy(image = img)
- })
- }
- .toSet
- val blackbox = config.fields.get("blackboxes")
- .map(_.convertTo[Set[ImageName]].map {
- image => ImageName(image.name, image.prefix.orElse(prefix), image.tag.orElse(tag))
- })
- Runtimes(runtimes, blackbox.getOrElse(Set.empty))
+ def publicImageName: String = {
+ val p = prefix.filter(_.nonEmpty).map(_ + "/").getOrElse("")
+ val t = tag.filter(_.nonEmpty).map(":" + _).getOrElse("")
+ p + name + t
}
/**
- * A runtime manifest describes the "exec" runtime support.
- *
- * @param kind the name of the kind e.g., nodejs:6
- * @param deprecated true iff the runtime is deprecated (allows get/delete but not create/update/invoke)
- * @param default true iff the runtime is the default kind for its family (nodejs:default -> nodejs:6)
- * @param attached true iff the source is an attachments (not inlined source)
- * @param requireMain true iff main entry point is not optional
- * @param sentinelledLogs true iff the runtime generates stdout/stderr log sentinels after an activation
- * @param image optional image name, otherwise inferred via fixed mapping (remove colons and append 'action')
+ * The internal name of the image for an action kind. It overrides
+ * the prefix with an internal name. Optionally overrides tag.
*/
- protected[core] case class RuntimeManifest(
- kind: String,
- image: ImageName,
- deprecated: Option[Boolean] = None,
- default: Option[Boolean] = None,
- attached: Option[Attached] = None,
- requireMain: Option[Boolean] = None,
- sentinelledLogs: Option[Boolean] = None) {
-
- protected[entity] def toJsonSummary = {
- JsObject(
- "kind" -> kind.toJson,
- "image" -> image.publicImageName.toJson,
- "deprecated" -> deprecated.getOrElse(false).toJson,
- "default" -> default.getOrElse(false).toJson,
- "attached" -> attached.isDefined.toJson,
- "requireMain" -> requireMain.getOrElse(false).toJson)
+ def localImageName(registry: String, prefix: String, tagOverride: Option[String] = None): String = {
+ val r = Option(registry)
+ .filter(_.nonEmpty)
+ .map { reg =>
+ if (reg.endsWith("/")) reg else reg + "/"
}
+ .getOrElse("")
+ val p = Option(prefix).filter(_.nonEmpty).map(_ + "/").getOrElse("")
+ r + p + name + ":" + tagOverride.orElse(tag).getOrElse(ImageName.defaultImageTag)
}
/**
- * An image name for an action refers to the container image canonically as
- * "prefix/name[:tag]" e.g., "openwhisk/python3action:latest".
+ * Overrides equals to allow match on undefined tag or when tag is latest
+ * in this or that.
*/
- protected[core] case class ImageName(
- name: String,
- prefix: Option[String] = None,
- tag: Option[String] = None) {
-
- /**
- * The name of the public image for an action kind.
- */
- def publicImageName: String = {
- val p = prefix.filter(_.nonEmpty).map(_ + "/").getOrElse("")
- val t = tag.filter(_.nonEmpty).map(":" + _).getOrElse("")
- p + name + t
- }
-
- /**
- * The internal name of the image for an action kind. It overrides
- * the prefix with an internal name. Optionally overrides tag.
- */
- def localImageName(registry: String, prefix: String, tagOverride: Option[String] = None): String = {
- val r = Option(registry).filter(_.nonEmpty).map { reg =>
- if (reg.endsWith("/")) reg else reg + "/"
- }.getOrElse("")
- val p = Option(prefix).filter(_.nonEmpty).map(_ + "/").getOrElse("")
- r + p + name + ":" + tagOverride.orElse(tag).getOrElse(ImageName.defaultImageTag)
- }
-
- /**
- * Overrides equals to allow match on undefined tag or when tag is latest
- * in this or that.
- */
- override def equals(that: Any) = that match {
- case ImageName(n, p, t) =>
- name == n && p == prefix && (t == tag || {
- val thisTag = tag.getOrElse(ImageName.defaultImageTag)
- val thatTag = t.getOrElse(ImageName.defaultImageTag)
- thisTag == thatTag
- })
+ override def equals(that: Any) = that match {
+ case ImageName(n, p, t) =>
+ name == n && p == prefix && (t == tag || {
+ val thisTag = tag.getOrElse(ImageName.defaultImageTag)
+ val thatTag = t.getOrElse(ImageName.defaultImageTag)
+ thisTag == thatTag
+ })
- case _ => false
- }
+ case _ => false
}
+ }
- protected[core] object ImageName {
- protected val defaultImageTag = "latest"
- private val componentRegex = """([a-z0-9._-]+)""".r
- private val tagRegex = """([\w.-]{0,128})""".r
-
- /**
- * Constructs an ImageName from a string. This method checks that the image name conforms
- * to the Docker naming. As a result, failure to deserialize a string will throw an exception
- * which fails the Try. Callers could use this to short-circuit operations (CRUD or activation).
- * Internal container names use the proper constructor directly.
- */
- def fromString(s: String): Try[ImageName] = Try {
- val parts = s.split("/")
+ protected[core] object ImageName {
+ protected val defaultImageTag = "latest"
+ private val componentRegex = """([a-z0-9._-]+)""".r
+ private val tagRegex = """([\w.-]{0,128})""".r
- val (name, tag) = parts.last.split(":") match {
- case Array(componentRegex(s)) => (s, None)
- case Array(componentRegex(s), tagRegex(t)) => (s, Some(t))
- case _ => throw DeserializationException("image name is not valid")
- }
+ /**
+ * Constructs an ImageName from a string. This method checks that the image name conforms
+ * to the Docker naming. As a result, failure to deserialize a string will throw an exception
+ * which fails the Try. Callers could use this to short-circuit operations (CRUD or activation).
+ * Internal container names use the proper constructor directly.
+ */
+ def fromString(s: String): Try[ImageName] =
+ Try {
+ val parts = s.split("/")
- val prefixParts = parts.dropRight(1)
- if (!prefixParts.forall(componentRegex.pattern.matcher(_).matches)) {
- throw DeserializationException("image prefix not is not valid")
- }
- val prefix = if (prefixParts.nonEmpty) Some(prefixParts.mkString("/")) else None
+ val (name, tag) = parts.last.split(":") match {
+ case Array(componentRegex(s)) => (s, None)
+ case Array(componentRegex(s), tagRegex(t)) => (s, Some(t))
+ case _ => throw DeserializationException("image name is not valid")
+ }
- ImageName(name, prefix, tag)
- } recoverWith {
- case t: DeserializationException => Failure(t)
- case t => Failure(DeserializationException("could not parse image name"))
+ val prefixParts = parts.dropRight(1)
+ if (!prefixParts.forall(componentRegex.pattern.matcher(_).matches)) {
+ throw DeserializationException("image prefix not is not valid")
}
- }
+ val prefix = if (prefixParts.nonEmpty) Some(prefixParts.mkString("/")) else None
- /**
- * A runtime family manifest is a collection of runtimes grouped by a family (e.g., swift with versions swift:2 and swift:3).
- * @param family runtime family
- * @version set of runtime manifests
- */
- protected[entity] case class RuntimeFamily(name: String, versions: Set[RuntimeManifest])
+ ImageName(name, prefix, tag)
+ } recoverWith {
+ case t: DeserializationException => Failure(t)
+ case t => Failure(DeserializationException("could not parse image name"))
+ }
+ }
- /**
- * A collection of runtime families.
- *
- * @param set of supported runtime families
- */
- protected[core] case class Runtimes(runtimes: Set[RuntimeFamily], blackboxImages: Set[ImageName]) {
+ /**
+ * A runtime family manifest is a collection of runtimes grouped by a family (e.g., swift with versions swift:2 and swift:3).
+ * @param family runtime family
+ * @version set of runtime manifests
+ */
+ protected[entity] case class RuntimeFamily(name: String, versions: Set[RuntimeManifest])
- val knownContainerRuntimes: Set[String] = runtimes.flatMap(_.versions.map(_.kind))
+ /**
+ * A collection of runtime families.
+ *
+ * @param set of supported runtime families
+ */
+ protected[core] case class Runtimes(runtimes: Set[RuntimeFamily], blackboxImages: Set[ImageName]) {
- def toJson: JsObject = {
- runtimes.map { family =>
- family.name -> family.versions.map(_.toJsonSummary)
- }.toMap.toJson.asJsObject
- }
+ val knownContainerRuntimes: Set[String] = runtimes.flatMap(_.versions.map(_.kind))
- def resolveDefaultRuntime(kind: String): Option[RuntimeManifest] = {
- kind match {
- case defaultSplitter(family) => defaultRuntimes.get(family).flatMap(manifests.get(_))
- case _ => manifests.get(kind)
- }
+ def toJson: JsObject = {
+ runtimes
+ .map { family =>
+ family.name -> family.versions.map(_.toJsonSummary)
}
+ .toMap
+ .toJson
+ .asJsObject
+ }
- val manifests: Map[String, RuntimeManifest] = {
- runtimes.flatMap {
- _.versions.map {
- m => m.kind -> m
- }
- }.toMap
- }
+ def resolveDefaultRuntime(kind: String): Option[RuntimeManifest] = {
+ kind match {
+ case defaultSplitter(family) => defaultRuntimes.get(family).flatMap(manifests.get(_))
+ case _ => manifests.get(kind)
+ }
+ }
- private val defaultRuntimes: Map[String, String] = {
- runtimes.map { family =>
- family.versions.filter(_.default.exists(identity)).toList match {
- case Nil if family.versions.size == 1 => family.name -> family.versions.head.kind
- case Nil => throw new IllegalArgumentException(s"${family.name} has multiple versions, but no default.")
- case d :: Nil => family.name -> d.kind
- case ds => throw new IllegalArgumentException(s"Found more than one default for ${family.name}: ${ds.mkString(",")}.")
- }
- }.toMap
+ val manifests: Map[String, RuntimeManifest] = {
+ runtimes.flatMap {
+ _.versions.map { m =>
+ m.kind -> m
}
+ }.toMap
+ }
- private val defaultSplitter = "([a-z0-9]+):default".r
+ private val defaultRuntimes: Map[String, String] = {
+ runtimes.map { family =>
+ family.versions.filter(_.default.exists(identity)).toList match {
+ case Nil if family.versions.size == 1 => family.name -> family.versions.head.kind
+ case Nil => throw new IllegalArgumentException(s"${family.name} has multiple versions, but no default.")
+ case d :: Nil => family.name -> d.kind
+ case ds =>
+ throw new IllegalArgumentException(s"Found more than one default for ${family.name}: ${ds.mkString(",")}.")
+ }
+ }.toMap
}
- protected[entity] implicit val imageNameSerdes = jsonFormat3(ImageName.apply)
- protected[entity] implicit val runtimeManifestSerdes = jsonFormat7(RuntimeManifest)
+ private val defaultSplitter = "([a-z0-9]+):default".r
+ }
+
+ protected[entity] implicit val imageNameSerdes = jsonFormat3(ImageName.apply)
+ protected[entity] implicit val runtimeManifestSerdes = jsonFormat7(RuntimeManifest)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/FullyQualifiedEntityName.scala b/common/scala/src/main/scala/whisk/core/entity/FullyQualifiedEntityName.scala
index 066b0df..a78045d 100644
--- a/common/scala/src/main/scala/whisk/core/entity/FullyQualifiedEntityName.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/FullyQualifiedEntityName.scala
@@ -30,66 +30,70 @@ import whisk.core.entity.size.SizeString
* - EntityName: the name of the entity
* - Version: the semantic version of the resource
*/
-protected[core] case class FullyQualifiedEntityName(path: EntityPath, name: EntityName, version: Option[SemVer] = None) extends ByteSizeable {
- private val qualifiedName: String = path + EntityPath.PATHSEP + name
- /** Resolves default namespace in path to given name if the root path is the default namespace. */
- def resolve(namespace: EntityName) = FullyQualifiedEntityName(path.resolveNamespace(namespace), name, version)
+protected[core] case class FullyQualifiedEntityName(path: EntityPath, name: EntityName, version: Option[SemVer] = None)
+ extends ByteSizeable {
+ private val qualifiedName: String = path + EntityPath.PATHSEP + name
- /** @return full path including name, i.e., "path/name" */
- def fullPath: EntityPath = path.addPath(name)
+ /** Resolves default namespace in path to given name if the root path is the default namespace. */
+ def resolve(namespace: EntityName) = FullyQualifiedEntityName(path.resolveNamespace(namespace), name, version)
- /**
- * Creates new fully qualified entity name that shifts the name into the path and adds a new name:
- * (p, n).add(x) -> (p/n, x).
- *
- * @return new fully qualified name
- */
- def add(n: EntityName) = FullyQualifiedEntityName(path.addPath(name), n)
+ /** @return full path including name, i.e., "path/name" */
+ def fullPath: EntityPath = path.addPath(name)
- def toDocId = DocId(qualifiedName)
- def namespace: EntityName = path.root
- def qualifiedNameWithLeadingSlash: String = EntityPath.PATHSEP + qualifiedName
- def asString = path.addPath(name) + version.map("@" + _.toString).getOrElse("")
+ /**
+ * Creates new fully qualified entity name that shifts the name into the path and adds a new name:
+ * (p, n).add(x) -> (p/n, x).
+ *
+ * @return new fully qualified name
+ */
+ def add(n: EntityName) = FullyQualifiedEntityName(path.addPath(name), n)
- override def size = qualifiedName.sizeInBytes
- override def toString = asString
- override def hashCode = qualifiedName.hashCode
+ def toDocId = DocId(qualifiedName)
+ def namespace: EntityName = path.root
+ def qualifiedNameWithLeadingSlash: String = EntityPath.PATHSEP + qualifiedName
+ def asString = path.addPath(name) + version.map("@" + _.toString).getOrElse("")
+
+ override def size = qualifiedName.sizeInBytes
+ override def toString = asString
+ override def hashCode = qualifiedName.hashCode
}
protected[core] object FullyQualifiedEntityName extends DefaultJsonProtocol {
- // must use jsonFormat with explicit field names and order because class extends a trait
- private val caseClassSerdes = jsonFormat(FullyQualifiedEntityName.apply _, "path", "name", "version")
+ // must use jsonFormat with explicit field names and order because class extends a trait
+ private val caseClassSerdes = jsonFormat(FullyQualifiedEntityName.apply _, "path", "name", "version")
- protected[core] val serdes = new RootJsonFormat[FullyQualifiedEntityName] {
- def write(n: FullyQualifiedEntityName) = caseClassSerdes.write(n)
+ protected[core] val serdes = new RootJsonFormat[FullyQualifiedEntityName] {
+ def write(n: FullyQualifiedEntityName) = caseClassSerdes.write(n)
- def read(value: JsValue) = Try {
- value match {
- case JsObject(fields) => caseClassSerdes.read(value)
- // tolerate dual serialization modes; Exec serializes a sequence of fully qualified names
- // by their document id which excludes the version (hence it is just a string)
- case JsString(name) => EntityPath(name).toFullyQualifiedEntityName
- case _ => deserializationError("fully qualified name malformed")
- }
- } match {
- case Success(s) => s
- case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)
- case Failure(t) => deserializationError("fully qualified name malformed")
+ def read(value: JsValue) =
+ Try {
+ value match {
+ case JsObject(fields) => caseClassSerdes.read(value)
+ // tolerate dual serialization modes; Exec serializes a sequence of fully qualified names
+ // by their document id which excludes the version (hence it is just a string)
+ case JsString(name) => EntityPath(name).toFullyQualifiedEntityName
+ case _ => deserializationError("fully qualified name malformed")
}
- }
+ } match {
+ case Success(s) => s
+ case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)
+ case Failure(t) => deserializationError("fully qualified name malformed")
+ }
+ }
- // alternate serializer that drops version
- protected[entity] val serdesAsDocId = new RootJsonFormat[FullyQualifiedEntityName] {
- def write(n: FullyQualifiedEntityName) = n.toDocId.toJson
- def read(value: JsValue) = Try {
- value match {
- case JsString(name) => EntityPath(name).toFullyQualifiedEntityName
- case _ => deserializationError("fully qualified name malformed")
- }
- } match {
- case Success(s) => s
- case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)
- case Failure(t) => deserializationError("fully qualified name malformed")
+ // alternate serializer that drops version
+ protected[entity] val serdesAsDocId = new RootJsonFormat[FullyQualifiedEntityName] {
+ def write(n: FullyQualifiedEntityName) = n.toDocId.toJson
+ def read(value: JsValue) =
+ Try {
+ value match {
+ case JsString(name) => EntityPath(name).toFullyQualifiedEntityName
+ case _ => deserializationError("fully qualified name malformed")
}
- }
+ } match {
+ case Success(s) => s
+ case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)
+ case Failure(t) => deserializationError("fully qualified name malformed")
+ }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Identity.scala b/common/scala/src/main/scala/whisk/core/entity/Identity.scala
index 32093c8..8e2ad05 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Identity.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Identity.scala
@@ -30,98 +30,104 @@ import whisk.core.database.StaleParameter
import whisk.core.entitlement.Privilege
import whisk.core.entitlement.Privilege.Privilege
-case class UserLimits(invocationsPerMinute: Option[Int] = None, concurrentInvocations: Option[Int] = None, firesPerMinute: Option[Int] = None)
+case class UserLimits(invocationsPerMinute: Option[Int] = None,
+ concurrentInvocations: Option[Int] = None,
+ firesPerMinute: Option[Int] = None)
object UserLimits extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat3(UserLimits.apply)
+ implicit val serdes = jsonFormat3(UserLimits.apply)
}
-protected[core] case class Identity(subject: Subject, namespace: EntityName, authkey: AuthKey, rights: Set[Privilege], limits: UserLimits = UserLimits()) {
- def uuid = authkey.uuid
+protected[core] case class Identity(subject: Subject,
+ namespace: EntityName,
+ authkey: AuthKey,
+ rights: Set[Privilege],
+ limits: UserLimits = UserLimits()) {
+ def uuid = authkey.uuid
}
object Identity extends MultipleReadersSingleWriterCache[Identity, DocInfo] with DefaultJsonProtocol {
- private val viewName = "subjects/identities"
+ private val viewName = "subjects/identities"
- override val cacheEnabled = true
- implicit val serdes = jsonFormat5(Identity.apply)
+ override val cacheEnabled = true
+ implicit val serdes = jsonFormat5(Identity.apply)
- /**
- * Retrieves a key for namespace.
- * There may be more than one key for the namespace, in which case,
- * one is picked arbitrarily.
- */
- def get(datastore: AuthStore, namespace: EntityName)(
- implicit transid: TransactionId): Future[Identity] = {
- implicit val logger: Logging = datastore.logging
- implicit val ec = datastore.executionContext
- val ns = namespace.asString
- val key = CacheKey(namespace)
+ /**
+ * Retrieves a key for namespace.
+ * There may be more than one key for the namespace, in which case,
+ * one is picked arbitrarily.
+ */
+ def get(datastore: AuthStore, namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {
+ implicit val logger: Logging = datastore.logging
+ implicit val ec = datastore.executionContext
+ val ns = namespace.asString
+ val key = CacheKey(namespace)
- cacheLookup(key, {
- list(datastore, List(ns), limit = 1) map { list =>
- list.length match {
- case 1 =>
- rowToIdentity(list.head, ns)
- case 0 =>
- logger.info(this, s"$viewName[$namespace] does not exist")
- throw new NoDocumentException("namespace does not exist")
- case _ =>
- logger.error(this, s"$viewName[$namespace] is not unique")
- throw new IllegalStateException("namespace is not unique")
- }
- }
- })
- }
-
- def get(datastore: AuthStore, authkey: AuthKey)(
- implicit transid: TransactionId): Future[Identity] = {
- implicit val logger: Logging = datastore.logging
- implicit val ec = datastore.executionContext
-
- cacheLookup(CacheKey(authkey), {
- list(datastore, List(authkey.uuid.asString, authkey.key.asString)) map { list =>
- list.length match {
- case 1 =>
- rowToIdentity(list.head, authkey.uuid.asString)
- case 0 =>
- logger.info(this, s"$viewName[${authkey.uuid}] does not exist")
- throw new NoDocumentException("uuid does not exist")
- case _ =>
- logger.error(this, s"$viewName[${authkey.uuid}] is not unique")
- throw new IllegalStateException("uuid is not unique")
- }
- }
- })
- }
+ cacheLookup(
+ key, {
+ list(datastore, List(ns), limit = 1) map { list =>
+ list.length match {
+ case 1 =>
+ rowToIdentity(list.head, ns)
+ case 0 =>
+ logger.info(this, s"$viewName[$namespace] does not exist")
+ throw new NoDocumentException("namespace does not exist")
+ case _ =>
+ logger.error(this, s"$viewName[$namespace] is not unique")
+ throw new IllegalStateException("namespace is not unique")
+ }
+ }
+ })
+ }
- def list(datastore: AuthStore, key: List[Any], limit: Int = 2)(
- implicit transid: TransactionId): Future[List[JsObject]] = {
- datastore.query(viewName,
- startKey = key,
- endKey = key,
- skip = 0,
- limit = limit,
- includeDocs = true,
- descending = true,
- reduce = false,
- stale = StaleParameter.No)
- }
+ def get(datastore: AuthStore, authkey: AuthKey)(implicit transid: TransactionId): Future[Identity] = {
+ implicit val logger: Logging = datastore.logging
+ implicit val ec = datastore.executionContext
- private def rowToIdentity(row: JsObject, key: String)(
- implicit transid: TransactionId, logger: Logging) = {
- row.getFields("id", "value", "doc") match {
- case Seq(JsString(id), JsObject(value), doc) =>
- val limits = Try(doc.convertTo[UserLimits]).getOrElse(UserLimits())
- val subject = Subject(id)
- val JsString(uuid) = value("uuid")
- val JsString(secret) = value("key")
- val JsString(namespace) = value("namespace")
- Identity(subject, EntityName(namespace), AuthKey(UUID(uuid), Secret(secret)), Privilege.ALL, limits)
+ cacheLookup(
+ CacheKey(authkey), {
+ list(datastore, List(authkey.uuid.asString, authkey.key.asString)) map { list =>
+ list.length match {
+ case 1 =>
+ rowToIdentity(list.head, authkey.uuid.asString)
+ case 0 =>
+ logger.info(this, s"$viewName[${authkey.uuid}] does not exist")
+ throw new NoDocumentException("uuid does not exist")
case _ =>
- logger.error(this, s"$viewName[$key] has malformed view '${row.compactPrint}'")
- throw new IllegalStateException("identities view malformed")
+ logger.error(this, s"$viewName[${authkey.uuid}] is not unique")
+ throw new IllegalStateException("uuid is not unique")
+ }
}
+ })
+ }
+
+ def list(datastore: AuthStore, key: List[Any], limit: Int = 2)(
+ implicit transid: TransactionId): Future[List[JsObject]] = {
+ datastore.query(
+ viewName,
+ startKey = key,
+ endKey = key,
+ skip = 0,
+ limit = limit,
+ includeDocs = true,
+ descending = true,
+ reduce = false,
+ stale = StaleParameter.No)
+ }
+
+ private def rowToIdentity(row: JsObject, key: String)(implicit transid: TransactionId, logger: Logging) = {
+ row.getFields("id", "value", "doc") match {
+ case Seq(JsString(id), JsObject(value), doc) =>
+ val limits = Try(doc.convertTo[UserLimits]).getOrElse(UserLimits())
+ val subject = Subject(id)
+ val JsString(uuid) = value("uuid")
+ val JsString(secret) = value("key")
+ val JsString(namespace) = value("namespace")
+ Identity(subject, EntityName(namespace), AuthKey(UUID(uuid), Secret(secret)), Privilege.ALL, limits)
+ case _ =>
+ logger.error(this, s"$viewName[$key] has malformed view '${row.compactPrint}'")
+ throw new IllegalStateException("identities view malformed")
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/InstanceId.scala b/common/scala/src/main/scala/whisk/core/entity/InstanceId.scala
index 03f3271..743cda5 100644
--- a/common/scala/src/main/scala/whisk/core/entity/InstanceId.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/InstanceId.scala
@@ -20,9 +20,9 @@ package whisk.core.entity
import spray.json.DefaultJsonProtocol
case class InstanceId(val instance: Int) {
- def toInt: Int = instance
+ def toInt: Int = instance
}
object InstanceId extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat1(InstanceId.apply)
+ implicit val serdes = jsonFormat1(InstanceId.apply)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Limits.scala b/common/scala/src/main/scala/whisk/core/entity/Limits.scala
index 1afa860..81a9902 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Limits.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Limits.scala
@@ -29,8 +29,8 @@ import spray.json.DefaultJsonProtocol
* that require global knowledge).
*/
protected[entity] abstract class Limits {
- protected[entity] def toJson: JsValue
- override def toString = toJson.compactPrint
+ protected[entity] def toJson: JsValue
+ override def toString = toJson.compactPrint
}
/**
@@ -45,46 +45,43 @@ protected[entity] abstract class Limits {
* @param memory the memory limit in megabytes, assured to be non-null because it is a value
* @param logs the limit for logs written by the container and stored in the activation record, assured to be non-null because it is a value
*/
-protected[core] case class ActionLimits protected[core] (timeout: TimeLimit, memory: MemoryLimit, logs: LogLimit) extends Limits {
- override protected[entity] def toJson = ActionLimits.serdes.write(this)
+protected[core] case class ActionLimits protected[core] (timeout: TimeLimit, memory: MemoryLimit, logs: LogLimit)
+ extends Limits {
+ override protected[entity] def toJson = ActionLimits.serdes.write(this)
}
/**
* Limits on a specific trigger. None yet.
*/
protected[core] case class TriggerLimits protected[core] () extends Limits {
- override protected[entity] def toJson: JsValue = TriggerLimits.serdes.write(this)
+ override protected[entity] def toJson: JsValue = TriggerLimits.serdes.write(this)
}
-protected[core] object ActionLimits
- extends ArgNormalizer[ActionLimits]
- with DefaultJsonProtocol {
+protected[core] object ActionLimits extends ArgNormalizer[ActionLimits] with DefaultJsonProtocol {
- /** Creates a ActionLimits instance with default duration, memory and log limits. */
- protected[core] def apply(): ActionLimits = ActionLimits(TimeLimit(), MemoryLimit(), LogLimit())
+ /** Creates a ActionLimits instance with default duration, memory and log limits. */
+ protected[core] def apply(): ActionLimits = ActionLimits(TimeLimit(), MemoryLimit(), LogLimit())
- override protected[core] implicit val serdes = new RootJsonFormat[ActionLimits] {
- val helper = jsonFormat3(ActionLimits.apply)
+ override protected[core] implicit val serdes = new RootJsonFormat[ActionLimits] {
+ val helper = jsonFormat3(ActionLimits.apply)
- def read(value: JsValue) = {
- val obj = Try {
- value.asJsObject.convertTo[Map[String, JsValue]]
- } getOrElse deserializationError("no valid json object passed")
+ def read(value: JsValue) = {
+ val obj = Try {
+ value.asJsObject.convertTo[Map[String, JsValue]]
+ } getOrElse deserializationError("no valid json object passed")
- val time = TimeLimit.serdes.read(obj.get("timeout") getOrElse deserializationError("'timeout' is missing"))
- val memory = MemoryLimit.serdes.read(obj.get("memory") getOrElse deserializationError("'memory' is missing"))
- val logs = obj.get("logs") map { LogLimit.serdes.read(_) } getOrElse LogLimit()
+ val time = TimeLimit.serdes.read(obj.get("timeout") getOrElse deserializationError("'timeout' is missing"))
+ val memory = MemoryLimit.serdes.read(obj.get("memory") getOrElse deserializationError("'memory' is missing"))
+ val logs = obj.get("logs") map { LogLimit.serdes.read(_) } getOrElse LogLimit()
- ActionLimits(time, memory, logs)
- }
-
- def write(a: ActionLimits) = helper.write(a)
+ ActionLimits(time, memory, logs)
}
+
+ def write(a: ActionLimits) = helper.write(a)
+ }
}
-protected[core] object TriggerLimits
- extends ArgNormalizer[TriggerLimits]
- with DefaultJsonProtocol {
+protected[core] object TriggerLimits extends ArgNormalizer[TriggerLimits] with DefaultJsonProtocol {
- override protected[core] implicit val serdes = jsonFormat0(TriggerLimits.apply)
+ override protected[core] implicit val serdes = jsonFormat0(TriggerLimits.apply)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/LogLimit.scala b/common/scala/src/main/scala/whisk/core/entity/LogLimit.scala
index ff27e01..0b0d88c 100644
--- a/common/scala/src/main/scala/whisk/core/entity/LogLimit.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/LogLimit.scala
@@ -41,42 +41,43 @@ import whisk.core.entity.size.SizeInt
* @param megabytes the memory limit in megabytes for the action
*/
protected[core] class LogLimit private (val megabytes: Int) extends AnyVal {
- protected[core] def asMegaBytes: ByteSize = megabytes.megabytes
+ protected[core] def asMegaBytes: ByteSize = megabytes.megabytes
}
protected[core] object LogLimit extends ArgNormalizer[LogLimit] {
- protected[core] val MIN_LOGSIZE = 0 MB
- protected[core] val MAX_LOGSIZE = 10 MB
- protected[core] val STD_LOGSIZE = 10 MB
+ protected[core] val MIN_LOGSIZE = 0 MB
+ protected[core] val MAX_LOGSIZE = 10 MB
+ protected[core] val STD_LOGSIZE = 10 MB
- /** Gets LogLimit with default log limit */
- protected[core] def apply(): LogLimit = LogLimit(STD_LOGSIZE)
+ /** Gets LogLimit with default log limit */
+ protected[core] def apply(): LogLimit = LogLimit(STD_LOGSIZE)
- /**
- * Creates LogLimit for limit. Only the default limit is allowed currently.
- *
- * @param megabytes the limit in megabytes, must be within permissible range
- * @return LogLimit with limit set
- * @throws IllegalArgumentException if limit does not conform to requirements
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(megabytes: ByteSize): LogLimit = {
- require(megabytes >= MIN_LOGSIZE, s"log size $megabytes below allowed threshold of $MIN_LOGSIZE")
- require(megabytes <= MAX_LOGSIZE, s"log size $megabytes exceeds allowed threshold of $MAX_LOGSIZE")
- new LogLimit(megabytes.toMB.toInt);
- }
+ /**
+ * Creates LogLimit for limit. Only the default limit is allowed currently.
+ *
+ * @param megabytes the limit in megabytes, must be within permissible range
+ * @return LogLimit with limit set
+ * @throws IllegalArgumentException if limit does not conform to requirements
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(megabytes: ByteSize): LogLimit = {
+ require(megabytes >= MIN_LOGSIZE, s"log size $megabytes below allowed threshold of $MIN_LOGSIZE")
+ require(megabytes <= MAX_LOGSIZE, s"log size $megabytes exceeds allowed threshold of $MAX_LOGSIZE")
+ new LogLimit(megabytes.toMB.toInt);
+ }
- override protected[core] implicit val serdes = new RootJsonFormat[LogLimit] {
- def write(m: LogLimit) = JsNumber(m.megabytes)
+ override protected[core] implicit val serdes = new RootJsonFormat[LogLimit] {
+ def write(m: LogLimit) = JsNumber(m.megabytes)
- def read(value: JsValue) = Try {
- val JsNumber(mb) = value
- require(mb.isWhole(), "log limit must be whole number")
- LogLimit(mb.intValue MB)
- } match {
- case Success(limit) => limit
- case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)
- case Failure(e: Throwable) => deserializationError("log limit malformed", e)
- }
- }
+ def read(value: JsValue) =
+ Try {
+ val JsNumber(mb) = value
+ require(mb.isWhole(), "log limit must be whole number")
+ LogLimit(mb.intValue MB)
+ } match {
+ case Success(limit) => limit
+ case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)
+ case Failure(e: Throwable) => deserializationError("log limit malformed", e)
+ }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/MemoryLimit.scala b/common/scala/src/main/scala/whisk/core/entity/MemoryLimit.scala
index f6e14d4..a1c2517 100644
--- a/common/scala/src/main/scala/whisk/core/entity/MemoryLimit.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/MemoryLimit.scala
@@ -38,42 +38,42 @@ import whisk.core.entity.size.SizeInt
*
* @param megabytes the memory limit in megabytes for the action
*/
-protected[entity] class MemoryLimit private (val megabytes: Int) extends AnyVal {
-}
+protected[entity] class MemoryLimit private (val megabytes: Int) extends AnyVal {}
protected[core] object MemoryLimit extends ArgNormalizer[MemoryLimit] {
- protected[core] val MIN_MEMORY = 128 MB
- protected[core] val MAX_MEMORY = 512 MB
- protected[core] val STD_MEMORY = 256 MB
+ protected[core] val MIN_MEMORY = 128 MB
+ protected[core] val MAX_MEMORY = 512 MB
+ protected[core] val STD_MEMORY = 256 MB
- /** Gets TimeLimit with default duration */
- protected[core] def apply(): MemoryLimit = MemoryLimit(STD_MEMORY)
+ /** Gets TimeLimit with default duration */
+ protected[core] def apply(): MemoryLimit = MemoryLimit(STD_MEMORY)
- /**
- * Creates MemoryLimit for limit, iff limit is within permissible range.
- *
- * @param megabytes the limit in megabytes, must be within permissible range
- * @return MemoryLimit with limit set
- * @throws IllegalArgumentException if limit does not conform to requirements
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(megabytes: ByteSize): MemoryLimit = {
- require(megabytes >= MIN_MEMORY, s"memory $megabytes below allowed threshold of $MIN_MEMORY")
- require(megabytes <= MAX_MEMORY, s"memory $megabytes exceeds allowed threshold of $MAX_MEMORY")
- new MemoryLimit(megabytes.toMB.toInt);
- }
+ /**
+ * Creates MemoryLimit for limit, iff limit is within permissible range.
+ *
+ * @param megabytes the limit in megabytes, must be within permissible range
+ * @return MemoryLimit with limit set
+ * @throws IllegalArgumentException if limit does not conform to requirements
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(megabytes: ByteSize): MemoryLimit = {
+ require(megabytes >= MIN_MEMORY, s"memory $megabytes below allowed threshold of $MIN_MEMORY")
+ require(megabytes <= MAX_MEMORY, s"memory $megabytes exceeds allowed threshold of $MAX_MEMORY")
+ new MemoryLimit(megabytes.toMB.toInt);
+ }
- override protected[core] implicit val serdes = new RootJsonFormat[MemoryLimit] {
- def write(m: MemoryLimit) = JsNumber(m.megabytes)
+ override protected[core] implicit val serdes = new RootJsonFormat[MemoryLimit] {
+ def write(m: MemoryLimit) = JsNumber(m.megabytes)
- def read(value: JsValue) = Try {
- val JsNumber(mb) = value
- require(mb.isWhole(), "memory limit must be whole number")
- MemoryLimit(mb.intValue MB)
- } match {
- case Success(limit) => limit
- case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)
- case Failure(e: Throwable) => deserializationError("memory limit malformed", e)
- }
- }
+ def read(value: JsValue) =
+ Try {
+ val JsNumber(mb) = value
+ require(mb.isWhole(), "memory limit must be whole number")
+ MemoryLimit(mb.intValue MB)
+ } match {
+ case Success(limit) => limit
+ case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)
+ case Failure(e: Throwable) => deserializationError("memory limit malformed", e)
+ }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Parameter.scala b/common/scala/src/main/scala/whisk/core/entity/Parameter.scala
index 03bb0cf..660c6e6 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Parameter.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Parameter.scala
@@ -31,83 +31,93 @@ import whisk.core.entity.size.SizeString
* @param key the parameter name, assured to be non-null because it is a value
* @param value the parameter value, assured to be non-null because it is a value
*/
-protected[core] class Parameters protected[entity] (
- private val params: Map[ParameterName, ParameterValue])
+protected[core] class Parameters protected[entity] (private val params: Map[ParameterName, ParameterValue])
extends AnyVal {
- /**
- * Calculates the size in Bytes of the Parameters-instance.
- *
- * @return Size of instance as ByteSize
- */
- def size = params.map {
+ /**
+ * Calculates the size in Bytes of the Parameters-instance.
+ *
+ * @return Size of instance as ByteSize
+ */
+ def size =
+ params
+ .map {
case (name, value) =>
- name.size + value.size
- }.foldLeft(0 B)(_ + _)
-
- protected[entity] def +(p: (ParameterName, ParameterValue)) = {
- Option(p) map { p => new Parameters(params + (p._1 -> p._2)) } getOrElse this
- }
-
- protected[entity] def +(p: ParameterName, v: ParameterValue) = {
- new Parameters(params + (p -> v))
- }
-
- /** Add parameters from p to existing map, overwriting existing values in case of overlap in keys. */
- protected[core] def ++(p: Parameters) = new Parameters(params ++ p.params)
-
- /** Remove parameter by name. */
- protected[core] def -(p: String) = {
- // wrap with try since parameter name may throw an exception for illegal p
- Try(new Parameters(params - new ParameterName(p))) getOrElse this
- }
-
- /** Gets list all defined parameters. */
- protected[core] def definedParameters: Set[String] = {
- params.keySet filter (params(_).isDefined) map (_.name)
- }
-
- protected[core] def toJsArray = JsArray(params map { p => JsObject("key" -> p._1.name.toJson, "value" -> p._2.value.toJson) } toSeq: _*)
- protected[core] def toJsObject = JsObject(params map { p => (p._1.name -> p._2.value.toJson) })
- override def toString = toJsArray.compactPrint
-
- /**
- * Converts parameters to JSON object and merge keys with payload if defined.
- * In case of overlap, the keys in the payload supersede.
- */
- protected[core] def merge(payload: Option[JsObject]): Some[JsObject] = {
- val args = payload getOrElse JsObject()
- Some { (toJsObject.fields ++ args.fields).toJson.asJsObject }
+ name.size + value.size
+ }
+ .foldLeft(0 B)(_ + _)
+
+ protected[entity] def +(p: (ParameterName, ParameterValue)) = {
+ Option(p) map { p =>
+ new Parameters(params + (p._1 -> p._2))
+ } getOrElse this
+ }
+
+ protected[entity] def +(p: ParameterName, v: ParameterValue) = {
+ new Parameters(params + (p -> v))
+ }
+
+ /** Add parameters from p to existing map, overwriting existing values in case of overlap in keys. */
+ protected[core] def ++(p: Parameters) = new Parameters(params ++ p.params)
+
+ /** Remove parameter by name. */
+ protected[core] def -(p: String) = {
+ // wrap with try since parameter name may throw an exception for illegal p
+ Try(new Parameters(params - new ParameterName(p))) getOrElse this
+ }
+
+ /** Gets list all defined parameters. */
+ protected[core] def definedParameters: Set[String] = {
+ params.keySet filter (params(_).isDefined) map (_.name)
+ }
+
+ protected[core] def toJsArray =
+ JsArray(params map { p =>
+ JsObject("key" -> p._1.name.toJson, "value" -> p._2.value.toJson)
+ } toSeq: _*)
+ protected[core] def toJsObject =
+ JsObject(params map { p =>
+ (p._1.name -> p._2.value.toJson)
+ })
+ override def toString = toJsArray.compactPrint
+
+ /**
+ * Converts parameters to JSON object and merge keys with payload if defined.
+ * In case of overlap, the keys in the payload supersede.
+ */
+ protected[core] def merge(payload: Option[JsObject]): Some[JsObject] = {
+ val args = payload getOrElse JsObject()
+ Some { (toJsObject.fields ++ args.fields).toJson.asJsObject }
+ }
+
+ /**
+ * Retrieves parameter by name if it exists.
+ */
+ protected[core] def get(p: String): Option[JsValue] = {
+ params.get(new ParameterName(p)).map(_.value)
+ }
+
+ /**
+ * Retrieves parameter by name if it exist. If value of parameter
+ * is a boolean, return its value else false.
+ */
+ protected[core] def asBool(p: String): Option[Boolean] = {
+ get(p) flatMap {
+ case JsBoolean(b) => Some(b)
+ case _ => None
}
-
- /**
- * Retrieves parameter by name if it exists.
- */
- protected[core] def get(p: String): Option[JsValue] = {
- params.get(new ParameterName(p)).map(_.value)
- }
-
- /**
- * Retrieves parameter by name if it exist. If value of parameter
- * is a boolean, return its value else false.
- */
- protected[core] def asBool(p: String): Option[Boolean] = {
- get(p) flatMap {
- case JsBoolean(b) => Some(b)
- case _ => None
- }
- }
-
- /**
- * Retrieves parameter by name if it exist. If value of parameter
- * is a string, return its value else none.
- */
- protected[core] def asString(p: String): Option[String] = {
- get(p) flatMap {
- case JsString(s) => Some(s)
- case _ => None
- }
+ }
+
+ /**
+ * Retrieves parameter by name if it exist. If value of parameter
+ * is a string, return its value else none.
+ */
+ protected[core] def asString(p: String): Option[String] = {
+ get(p) flatMap {
+ case JsString(s) => Some(s)
+ case _ => None
}
+ }
}
/**
@@ -121,10 +131,11 @@ protected[core] class Parameters protected[entity] (
* @param name the name of the parameter (its key)
*/
protected[entity] class ParameterName protected[entity] (val name: String) extends AnyVal {
- /**
- * The size of the ParameterName entity as ByteSize.
- */
- def size = name sizeInBytes
+
+ /**
+ * The size of the ParameterName entity as ByteSize.
+ */
+ def size = name sizeInBytes
}
/**
@@ -140,92 +151,92 @@ protected[entity] class ParameterName protected[entity] (val name: String) exten
* @param value the value of the parameter, may be null
*/
protected[entity] class ParameterValue protected[entity] (private val v: JsValue) extends AnyVal {
- /** @return JsValue if defined else JsNull. */
- protected[entity] def value = Option(v) getOrElse JsNull
- /** @return true iff value is not JsNull. */
- protected[entity] def isDefined = value != JsNull
+ /** @return JsValue if defined else JsNull. */
+ protected[entity] def value = Option(v) getOrElse JsNull
- /**
- * The size of the ParameterValue entity as ByteSize.
- */
- def size = value.toString.sizeInBytes
+ /** @return true iff value is not JsNull. */
+ protected[entity] def isDefined = value != JsNull
+
+ /**
+ * The size of the ParameterValue entity as ByteSize.
+ */
+ def size = value.toString.sizeInBytes
}
protected[core] object Parameters extends ArgNormalizer[Parameters] {
- /** Name of parameter that indicates if action is a feed. */
- protected[core] val Feed = "feed"
- protected[core] val sizeLimit = 1 MB
-
- protected[core] def apply(): Parameters = new Parameters(Map())
+ /** Name of parameter that indicates if action is a feed. */
+ protected[core] val Feed = "feed"
+ protected[core] val sizeLimit = 1 MB
+
+ protected[core] def apply(): Parameters = new Parameters(Map())
+
+ /**
+ * Creates a parameter tuple from a pair of strings.
+ * A convenience method for tests.
+ *
+ * @param p the parameter name
+ * @param v the parameter value
+ * @return (ParameterName, ParameterValue)
+ * @throws IllegalArgumentException if key is not defined
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(p: String, v: String): Parameters = {
+ require(p != null && p.trim.nonEmpty, "key undefined")
+ Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
+ new ParameterValue(Option(v) map { _.trim.toJson } getOrElse JsNull))
+ }
+
+ /**
+ * Creates a parameter tuple from a parameter name and JsValue.
+ *
+ * @param p the parameter name
+ * @param v the parameter value
+ * @return (ParameterName, ParameterValue)
+ * @throws IllegalArgumentException if key is not defined
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(p: String, v: JsValue): Parameters = {
+ require(p != null && p.trim.nonEmpty, "key undefined")
+ Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
+ new ParameterValue(Option(v) getOrElse JsNull))
+ }
+
+ override protected[core] implicit val serdes = new RootJsonFormat[Parameters] {
+ def write(p: Parameters) = p.toJsArray
/**
- * Creates a parameter tuple from a pair of strings.
- * A convenience method for tests.
+ * Gets parameters as a Parameters instances. The argument should be a JArray
+ * [{key,value}], otherwise an IllegalParameter is thrown.
*
- * @param p the parameter name
- * @param v the parameter value
- * @return (ParameterName, ParameterValue)
- * @throws IllegalArgumentException if key is not defined
+ * @param parameters the JSON representation of an parameter array
+ * @return Parameters instance if parameters conforms to schema
*/
- @throws[IllegalArgumentException]
- protected[core] def apply(p: String, v: String): Parameters = {
- require(p != null && p.trim.nonEmpty, "key undefined")
- Parameters() + (
- new ParameterName(ArgNormalizer.trim(p)),
- new ParameterValue(Option(v) map { _.trim.toJson } getOrElse JsNull))
- }
+ def read(value: JsValue) =
+ Try {
+ val JsArray(params) = value
+ params
+ } flatMap {
+ read(_)
+ } getOrElse deserializationError("parameters malformed!")
/**
- * Creates a parameter tuple from a parameter name and JsValue.
+ * Gets parameters as a Parameters instances.
+ * The argument should be a [{key,value}].
*
- * @param p the parameter name
- * @param v the parameter value
- * @return (ParameterName, ParameterValue)
- * @throws IllegalArgumentException if key is not defined
+ * @param parameters the JSON representation of an parameter array
+ * @return Parameters instance if parameters conforms to schema
*/
- @throws[IllegalArgumentException]
- protected[core] def apply(p: String, v: JsValue): Parameters = {
- require(p != null && p.trim.nonEmpty, "key undefined")
- Parameters() + (
- new ParameterName(ArgNormalizer.trim(p)),
- new ParameterValue(Option(v) getOrElse JsNull))
- }
-
- override protected[core] implicit val serdes = new RootJsonFormat[Parameters] {
- def write(p: Parameters) = p.toJsArray
-
- /**
- * Gets parameters as a Parameters instances. The argument should be a JArray
- * [{key,value}], otherwise an IllegalParameter is thrown.
- *
- * @param parameters the JSON representation of an parameter array
- * @return Parameters instance if parameters conforms to schema
- */
- def read(value: JsValue) = Try {
- val JsArray(params) = value
- params
- } flatMap {
- read(_)
- } getOrElse deserializationError("parameters malformed!")
-
- /**
- * Gets parameters as a Parameters instances.
- * The argument should be a [{key,value}].
- *
- * @param parameters the JSON representation of an parameter array
- * @return Parameters instance if parameters conforms to schema
- */
- def read(params: Vector[JsValue]) = Try {
- new Parameters(params map {
- _.asJsObject.getFields("key", "value") match {
- case Seq(JsString(k), v: JsValue) =>
- val key = new ParameterName(k)
- val value = new ParameterValue(v)
- (key, value)
- }
- } toMap)
+ def read(params: Vector[JsValue]) = Try {
+ new Parameters(params map {
+ _.asJsObject.getFields("key", "value") match {
+ case Seq(JsString(k), v: JsValue) =>
+ val key = new ParameterName(k)
+ val value = new ParameterValue(v)
+ (key, value)
}
+ } toMap)
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Secret.scala b/common/scala/src/main/scala/whisk/core/entity/Secret.scala
index 196dd87..db902ae 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Secret.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Secret.scala
@@ -33,49 +33,51 @@ import spray.json.deserializationError
* @param key the secret key, required not null or empty
*/
protected[core] class Secret private (val key: String) extends AnyVal {
- protected[core] def asString = toString
- protected[entity] def toJson = JsString(toString)
- override def toString = key
+ protected[core] def asString = toString
+ protected[entity] def toJson = JsString(toString)
+ override def toString = key
}
protected[core] object Secret extends ArgNormalizer[Secret] {
- /** Minimum secret length */
- private val MIN_LENGTH = 64
- /** Maximum secret length */
- private val MAX_LENGTH = 64
+ /** Minimum secret length */
+ private val MIN_LENGTH = 64
- /**
- * Creates a Secret from a string. The string must be a valid secret already.
- *
- * @param str the secret as string, at least 64 characters
- * @return Secret instance
- * @throws IllegalArgumentException is argument is not a valid Secret
- */
- @throws[IllegalArgumentException]
- override protected[entity] def factory(str: String): Secret = {
- require(str.length >= MIN_LENGTH, s"secret must be at least $MIN_LENGTH characters")
- require(str.length <= MAX_LENGTH, s"secret must be at most $MAX_LENGTH characters")
- new Secret(str)
- }
+ /** Maximum secret length */
+ private val MAX_LENGTH = 64
- /**
- * Creates a new random secret.
- *
- * @return Secret
- */
- protected[core] def apply(): Secret = {
- Secret(rand.alphanumeric.take(MIN_LENGTH).mkString)
- }
+ /**
+ * Creates a Secret from a string. The string must be a valid secret already.
+ *
+ * @param str the secret as string, at least 64 characters
+ * @return Secret instance
+ * @throws IllegalArgumentException is argument is not a valid Secret
+ */
+ @throws[IllegalArgumentException]
+ override protected[entity] def factory(str: String): Secret = {
+ require(str.length >= MIN_LENGTH, s"secret must be at least $MIN_LENGTH characters")
+ require(str.length <= MAX_LENGTH, s"secret must be at most $MAX_LENGTH characters")
+ new Secret(str)
+ }
- implicit val serdes = new RootJsonFormat[Secret] {
- def write(s: Secret) = s.toJson
+ /**
+ * Creates a new random secret.
+ *
+ * @return Secret
+ */
+ protected[core] def apply(): Secret = {
+ Secret(rand.alphanumeric.take(MIN_LENGTH).mkString)
+ }
- def read(value: JsValue) = Try {
- val JsString(s) = value
- Secret(s)
- } getOrElse deserializationError("secret malformed")
- }
+ implicit val serdes = new RootJsonFormat[Secret] {
+ def write(s: Secret) = s.toJson
- private val rand = new scala.util.Random(new java.security.SecureRandom())
+ def read(value: JsValue) =
+ Try {
+ val JsString(s) = value
+ Secret(s)
+ } getOrElse deserializationError("secret malformed")
+ }
+
+ private val rand = new scala.util.Random(new java.security.SecureRandom())
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/SemVer.scala b/common/scala/src/main/scala/whisk/core/entity/SemVer.scala
index ad8eebf..6038bd0 100644
--- a/common/scala/src/main/scala/whisk/core/entity/SemVer.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/SemVer.scala
@@ -36,66 +36,67 @@ import scala.util.Try
*/
protected[core] class SemVer private (private val version: (Int, Int, Int)) extends AnyVal {
- protected[core] def major = version._1
- protected[core] def minor = version._2
- protected[core] def patch = version._3
+ protected[core] def major = version._1
+ protected[core] def minor = version._2
+ protected[core] def patch = version._3
- protected[core] def upMajor = SemVer(major + 1, minor, patch)
- protected[core] def upMinor = SemVer(major, minor + 1, patch)
- protected[core] def upPatch = SemVer(major, minor, patch + 1)
+ protected[core] def upMajor = SemVer(major + 1, minor, patch)
+ protected[core] def upMinor = SemVer(major, minor + 1, patch)
+ protected[core] def upPatch = SemVer(major, minor, patch + 1)
- protected[entity] def toJson = JsString(toString)
- override def toString = s"$major.$minor.$patch"
+ protected[entity] def toJson = JsString(toString)
+ override def toString = s"$major.$minor.$patch"
}
protected[core] object SemVer {
- protected[core] val REGEX = """(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"""
+ protected[core] val REGEX = """(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"""
- /** Default semantic version */
- protected[core] def apply() = new SemVer(0, 0, 1)
+ /** Default semantic version */
+ protected[core] def apply() = new SemVer(0, 0, 1)
- /**
- * Creates a semantic version.
- *
- * @param major the major >= 0
- * @param minor the minor >= 0
- * @param patch the patch >= 0
- * @return SemVer instance
- * @throws IllegalArgumentException if the parameters results in an invalid semantic version
- */
- protected[core] def apply(major: Int, minor: Int, patch: Int): SemVer = {
- if ((major == 0 && minor == 0 && patch == 0) || (major < 0 || minor < 0 || patch < 0)) {
- throw new IllegalArgumentException(s"bad semantic version $major.$minor.$patch must not be negative")
- } else new SemVer(major, minor, patch)
- }
+ /**
+ * Creates a semantic version.
+ *
+ * @param major the major >= 0
+ * @param minor the minor >= 0
+ * @param patch the patch >= 0
+ * @return SemVer instance
+ * @throws IllegalArgumentException if the parameters results in an invalid semantic version
+ */
+ protected[core] def apply(major: Int, minor: Int, patch: Int): SemVer = {
+ if ((major == 0 && minor == 0 && patch == 0) || (major < 0 || minor < 0 || patch < 0)) {
+ throw new IllegalArgumentException(s"bad semantic version $major.$minor.$patch must not be negative")
+ } else new SemVer(major, minor, patch)
+ }
- /**
- * Parses a string representation of a semantic version and creates
- * a instance of SemVer. If the string is not properly formatted, an
- * IllegalSemVer exception is thrown.
- *
- * @param str to parse to extract semantic version
- * @return SemVer instance
- * @thrown IllegalArgumentException if string is not a valid semantic version
- */
- protected[entity] def apply(str: String): SemVer = {
- try {
- val parts = if (str != null && str.nonEmpty) str.split('.') else Array[String]()
- val major = if (parts.size >= 1) parts(0).toInt else 0
- val minor = if (parts.size >= 2) parts(1).toInt else 0
- val patch = if (parts.size >= 3) parts(2).toInt else 0
- SemVer(major, minor, patch)
- } catch {
- case _: Throwable => throw new IllegalArgumentException(s"bad semantic version $str")
- }
+ /**
+ * Parses a string representation of a semantic version and creates
+ * a instance of SemVer. If the string is not properly formatted, an
+ * IllegalSemVer exception is thrown.
+ *
+ * @param str to parse to extract semantic version
+ * @return SemVer instance
+ * @thrown IllegalArgumentException if string is not a valid semantic version
+ */
+ protected[entity] def apply(str: String): SemVer = {
+ try {
+ val parts = if (str != null && str.nonEmpty) str.split('.') else Array[String]()
+ val major = if (parts.size >= 1) parts(0).toInt else 0
+ val minor = if (parts.size >= 2) parts(1).toInt else 0
+ val patch = if (parts.size >= 3) parts(2).toInt else 0
+ SemVer(major, minor, patch)
+ } catch {
+ case _: Throwable => throw new IllegalArgumentException(s"bad semantic version $str")
}
+ }
- implicit val serdes = new RootJsonFormat[SemVer] {
- def write(v: SemVer) = v.toJson
+ implicit val serdes = new RootJsonFormat[SemVer] {
+ def write(v: SemVer) = v.toJson
- def read(value: JsValue) = Try {
- val JsString(v) = value
- SemVer(v)
- } getOrElse deserializationError("semantic version malformed")
- }
+ def read(value: JsValue) =
+ Try {
+ val JsString(v) = value
+ SemVer(v)
+ } getOrElse deserializationError("semantic version malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Size.scala b/common/scala/src/main/scala/whisk/core/entity/Size.scala
index f9cc3f2..6eea28c 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Size.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Size.scala
@@ -21,107 +21,108 @@ import java.nio.charset.StandardCharsets
object SizeUnits extends Enumeration {
- sealed abstract class Unit() {
- def toBytes(n: Long): Long
- def toKBytes(n: Long): Long
- def toMBytes(n: Long): Long
- }
-
- case object BYTE extends Unit {
- def toBytes(n: Long): Long = n
- def toKBytes(n: Long): Long = n / 1024
- def toMBytes(n: Long): Long = n / 1024 / 1024
- }
- case object KB extends Unit {
- def toBytes(n: Long): Long = n * 1024
- def toKBytes(n: Long): Long = n
- def toMBytes(n: Long): Long = n / 1024
- }
- case object MB extends Unit {
- def toBytes(n: Long): Long = n * 1024 * 1024
- def toKBytes(n: Long): Long = n * 1024
- def toMBytes(n: Long): Long = n
- }
+ sealed abstract class Unit() {
+ def toBytes(n: Long): Long
+ def toKBytes(n: Long): Long
+ def toMBytes(n: Long): Long
+ }
+
+ case object BYTE extends Unit {
+ def toBytes(n: Long): Long = n
+ def toKBytes(n: Long): Long = n / 1024
+ def toMBytes(n: Long): Long = n / 1024 / 1024
+ }
+ case object KB extends Unit {
+ def toBytes(n: Long): Long = n * 1024
+ def toKBytes(n: Long): Long = n
+ def toMBytes(n: Long): Long = n / 1024
+ }
+ case object MB extends Unit {
+ def toBytes(n: Long): Long = n * 1024 * 1024
+ def toKBytes(n: Long): Long = n * 1024
+ def toMBytes(n: Long): Long = n
+ }
}
case class ByteSize(size: Long, unit: SizeUnits.Unit) extends Ordered[ByteSize] {
- require(size >= 0, "a negative size of an object is not allowed.")
+ require(size >= 0, "a negative size of an object is not allowed.")
- def toBytes = unit.toBytes(size)
- def toKB = unit.toKBytes(size)
- def toMB = unit.toMBytes(size)
+ def toBytes = unit.toBytes(size)
+ def toKB = unit.toKBytes(size)
+ def toMB = unit.toMBytes(size)
- def +(other: ByteSize): ByteSize = {
- val commonUnit = SizeUnits.BYTE
- val commonSize = other.toBytes + toBytes
- ByteSize(commonSize, commonUnit)
- }
+ def +(other: ByteSize): ByteSize = {
+ val commonUnit = SizeUnits.BYTE
+ val commonSize = other.toBytes + toBytes
+ ByteSize(commonSize, commonUnit)
+ }
- def -(other: ByteSize): ByteSize = {
- val commonUnit = SizeUnits.BYTE
- val commonSize = toBytes - other.toBytes
- ByteSize(commonSize, commonUnit)
- }
+ def -(other: ByteSize): ByteSize = {
+ val commonUnit = SizeUnits.BYTE
+ val commonSize = toBytes - other.toBytes
+ ByteSize(commonSize, commonUnit)
+ }
- def compare(other: ByteSize) = toBytes compare other.toBytes
+ def compare(other: ByteSize) = toBytes compare other.toBytes
- override def toString = {
- unit match {
- case SizeUnits.BYTE => s"$size B"
- case SizeUnits.KB => s"$size KB"
- case SizeUnits.MB => s"$size MB"
- }
+ override def toString = {
+ unit match {
+ case SizeUnits.BYTE => s"$size B"
+ case SizeUnits.KB => s"$size KB"
+ case SizeUnits.MB => s"$size MB"
}
+ }
}
object ByteSize {
- def fromString(sizeString: String): ByteSize = {
- val unitprefix = sizeString.takeRight(1)
- val size = sizeString.dropRight(1).toLong
-
- val unit = unitprefix match {
- case "B" => SizeUnits.BYTE
- case "K" => SizeUnits.KB
- case "M" => SizeUnits.MB
- case _ => throw new IllegalArgumentException("""Size Unit not supported. Only "B", "K" and "M" are supported.""")
- }
-
- ByteSize(size, unit)
+ def fromString(sizeString: String): ByteSize = {
+ val unitprefix = sizeString.takeRight(1)
+ val size = sizeString.dropRight(1).toLong
+
+ val unit = unitprefix match {
+ case "B" => SizeUnits.BYTE
+ case "K" => SizeUnits.KB
+ case "M" => SizeUnits.MB
+ case _ => throw new IllegalArgumentException("""Size Unit not supported. Only "B", "K" and "M" are supported.""")
}
+
+ ByteSize(size, unit)
+ }
}
object size {
- implicit class SizeInt(n: Int) extends SizeConversion {
- def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n, unit)
- }
-
- implicit class SizeLong(n: Long) extends SizeConversion {
- def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n, unit)
- }
-
- implicit class SizeString(n: String) extends SizeConversion {
- def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n.getBytes(StandardCharsets.UTF_8).length, unit)
- }
-
- implicit class SizeOptionString(n: Option[String]) extends SizeConversion {
- def sizeIn(unit: SizeUnits.Unit): ByteSize = n map { s =>
- s.sizeIn(unit)
- } getOrElse {
- ByteSize(0, unit)
- }
- }
+ implicit class SizeInt(n: Int) extends SizeConversion {
+ def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n, unit)
+ }
+
+ implicit class SizeLong(n: Long) extends SizeConversion {
+ def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n, unit)
+ }
+
+ implicit class SizeString(n: String) extends SizeConversion {
+ def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n.getBytes(StandardCharsets.UTF_8).length, unit)
+ }
+
+ implicit class SizeOptionString(n: Option[String]) extends SizeConversion {
+ def sizeIn(unit: SizeUnits.Unit): ByteSize =
+ n map { s =>
+ s.sizeIn(unit)
+ } getOrElse {
+ ByteSize(0, unit)
+ }
+ }
}
trait SizeConversion {
- def B = sizeIn(SizeUnits.BYTE)
- def KB = sizeIn(SizeUnits.KB)
- def MB = sizeIn(SizeUnits.MB)
- def bytes = B
- def kilobytes = KB
- def megabytes = MB
+ def B = sizeIn(SizeUnits.BYTE)
+ def KB = sizeIn(SizeUnits.KB)
+ def MB = sizeIn(SizeUnits.MB)
+ def bytes = B
+ def kilobytes = KB
+ def megabytes = MB
- def sizeInBytes = sizeIn(SizeUnits.BYTE)
+ def sizeInBytes = sizeIn(SizeUnits.BYTE)
- def sizeIn(unit: SizeUnits.Unit): ByteSize
+ def sizeIn(unit: SizeUnits.Unit): ByteSize
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Subject.scala b/common/scala/src/main/scala/whisk/core/entity/Subject.scala
index d0e78cd..df6bcf9 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Subject.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Subject.scala
@@ -25,45 +25,47 @@ import spray.json.RootJsonFormat
import spray.json.deserializationError
protected[core] class Subject private (private val subject: String) extends AnyVal {
- protected[core] def asString = subject // to make explicit that this is a string conversion
- protected[entity] def toJson = JsString(subject)
- override def toString = subject
+ protected[core] def asString = subject // to make explicit that this is a string conversion
+ protected[entity] def toJson = JsString(subject)
+ override def toString = subject
}
protected[core] object Subject extends ArgNormalizer[Subject] {
- /** Minimum subject length */
- protected[core] val MIN_LENGTH = 5
- /**
- * Creates a Subject from a string.
- *
- * @param str the subject name, at least 6 characters
- * @return Subject instance
- * @throws IllegalArgumentException is argument is undefined
- */
- @throws[IllegalArgumentException]
- override protected[entity] def factory(str: String): Subject = {
- require(str.length >= MIN_LENGTH, s"subject must be at least $MIN_LENGTH characters")
- new Subject(str)
- }
+ /** Minimum subject length */
+ protected[core] val MIN_LENGTH = 5
- /**
- * Creates a random subject
- *
- * @return Subject
- */
- protected[core] def apply(): Subject = {
- Subject("anon-" + rand.alphanumeric.take(27).mkString)
- }
+ /**
+ * Creates a Subject from a string.
+ *
+ * @param str the subject name, at least 6 characters
+ * @return Subject instance
+ * @throws IllegalArgumentException is argument is undefined
+ */
+ @throws[IllegalArgumentException]
+ override protected[entity] def factory(str: String): Subject = {
+ require(str.length >= MIN_LENGTH, s"subject must be at least $MIN_LENGTH characters")
+ new Subject(str)
+ }
- override protected[core] implicit val serdes = new RootJsonFormat[Subject] {
- def write(s: Subject) = s.toJson
+ /**
+ * Creates a random subject
+ *
+ * @return Subject
+ */
+ protected[core] def apply(): Subject = {
+ Subject("anon-" + rand.alphanumeric.take(27).mkString)
+ }
- def read(value: JsValue) = Try {
- val JsString(s) = value
- Subject(s)
- } getOrElse deserializationError("subject malformed")
- }
+ override protected[core] implicit val serdes = new RootJsonFormat[Subject] {
+ def write(s: Subject) = s.toJson
- private val rand = new scala.util.Random()
+ def read(value: JsValue) =
+ Try {
+ val JsString(s) = value
+ Subject(s)
+ } getOrElse deserializationError("subject malformed")
+ }
+
+ private val rand = new scala.util.Random()
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/TimeLimit.scala b/common/scala/src/main/scala/whisk/core/entity/TimeLimit.scala
index f7b4741..3122e2e 100644
--- a/common/scala/src/main/scala/whisk/core/entity/TimeLimit.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/TimeLimit.scala
@@ -42,44 +42,49 @@ import spray.json.deserializationError
* @param duration the duration for the action, required not null
*/
protected[entity] class TimeLimit private (val duration: FiniteDuration) extends AnyVal {
- protected[core] def millis = duration.toMillis.toInt
- override def toString = duration.toString
+ protected[core] def millis = duration.toMillis.toInt
+ override def toString = duration.toString
}
protected[core] object TimeLimit extends ArgNormalizer[TimeLimit] {
- protected[core] val MIN_DURATION = 100 milliseconds
- protected[core] val MAX_DURATION = 5 minutes
- protected[core] val STD_DURATION = 1 minute
+ protected[core] val MIN_DURATION = 100 milliseconds
+ protected[core] val MAX_DURATION = 5 minutes
+ protected[core] val STD_DURATION = 1 minute
- /** Gets TimeLimit with default duration */
- protected[core] def apply(): TimeLimit = TimeLimit(STD_DURATION)
+ /** Gets TimeLimit with default duration */
+ protected[core] def apply(): TimeLimit = TimeLimit(STD_DURATION)
- /**
- * Creates TimeLimit for duration, iff duration is within permissible range.
- *
- * @param duration the duration in milliseconds, must be within permissible range
- * @return TimeLimit with duration set
- * @throws IllegalArgumentException if duration does not conform to requirements
- */
- @throws[IllegalArgumentException]
- protected[core] def apply(duration: FiniteDuration): TimeLimit = {
- require(duration != null, s"duration undefined")
- require(duration >= MIN_DURATION, s"duration ${duration.toMillis} milliseconds below allowed threshold of ${MIN_DURATION.toMillis} milliseconds")
- require(duration <= MAX_DURATION, s"duration ${duration.toMillis} milliseconds exceeds allowed threshold of ${MAX_DURATION.toMillis} milliseconds")
- new TimeLimit(duration)
- }
+ /**
+ * Creates TimeLimit for duration, iff duration is within permissible range.
+ *
+ * @param duration the duration in milliseconds, must be within permissible range
+ * @return TimeLimit with duration set
+ * @throws IllegalArgumentException if duration does not conform to requirements
+ */
+ @throws[IllegalArgumentException]
+ protected[core] def apply(duration: FiniteDuration): TimeLimit = {
+ require(duration != null, s"duration undefined")
+ require(
+ duration >= MIN_DURATION,
+ s"duration ${duration.toMillis} milliseconds below allowed threshold of ${MIN_DURATION.toMillis} milliseconds")
+ require(
+ duration <= MAX_DURATION,
+ s"duration ${duration.toMillis} milliseconds exceeds allowed threshold of ${MAX_DURATION.toMillis} milliseconds")
+ new TimeLimit(duration)
+ }
- override protected[core] implicit val serdes = new RootJsonFormat[TimeLimit] {
- def write(t: TimeLimit) = JsNumber(t.millis)
+ override protected[core] implicit val serdes = new RootJsonFormat[TimeLimit] {
+ def write(t: TimeLimit) = JsNumber(t.millis)
- def read(value: JsValue) = Try {
- val JsNumber(ms) = value
- require(ms.isWhole, "time limit must be whole number")
- TimeLimit(Duration(ms.intValue, MILLISECONDS))
- } match {
- case Success(limit) => limit
- case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)
- case Failure(e: Throwable) => deserializationError("time limit malformed", e)
- }
- }
+ def read(value: JsValue) =
+ Try {
+ val JsNumber(ms) = value
+ require(ms.isWhole, "time limit must be whole number")
+ TimeLimit(Duration(ms.intValue, MILLISECONDS))
+ } match {
+ case Success(limit) => limit
+ case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)
+ case Failure(e: Throwable) => deserializationError("time limit malformed", e)
+ }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/UUID.scala b/common/scala/src/main/scala/whisk/core/entity/UUID.scala
index b84d03c..11b56b4 100644
--- a/common/scala/src/main/scala/whisk/core/entity/UUID.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/UUID.scala
@@ -34,38 +34,40 @@ import spray.json.deserializationError
* @param uuid the uuid, required not null
*/
protected[core] class UUID private (private val uuid: java.util.UUID) extends AnyVal {
- protected[core] def asString = toString
- protected[core] def snippet = toString.substring(0, 8)
- protected[entity] def toJson = JsString(toString)
- override def toString = uuid.toString
+ protected[core] def asString = toString
+ protected[core] def snippet = toString.substring(0, 8)
+ protected[entity] def toJson = JsString(toString)
+ override def toString = uuid.toString
}
protected[core] object UUID extends ArgNormalizer[UUID] {
- /**
- * Creates a UUID from a string. The string must be a valid UUID.
- *
- * @param str the uuid as string
- * @return UUID instance
- * @throws IllegalArgumentException is argument is not a valid UUID
- */
- @throws[IllegalArgumentException]
- override protected[entity] def factory(str: String): UUID = {
- new UUID(java.util.UUID.fromString(str))
- }
- /**
- * Generates a random UUID using java.util.UUID factory.
- *
- * @return new UUID
- */
- protected[core] def apply(): UUID = new UUID(java.util.UUID.randomUUID())
+ /**
+ * Creates a UUID from a string. The string must be a valid UUID.
+ *
+ * @param str the uuid as string
+ * @return UUID instance
+ * @throws IllegalArgumentException is argument is not a valid UUID
+ */
+ @throws[IllegalArgumentException]
+ override protected[entity] def factory(str: String): UUID = {
+ new UUID(java.util.UUID.fromString(str))
+ }
- implicit val serdes = new RootJsonFormat[UUID] {
- def write(u: UUID) = u.toJson
+ /**
+ * Generates a random UUID using java.util.UUID factory.
+ *
+ * @return new UUID
+ */
+ protected[core] def apply(): UUID = new UUID(java.util.UUID.randomUUID())
- def read(value: JsValue) = Try {
- val JsString(u) = value
- UUID(u)
- } getOrElse deserializationError("uuid malformed")
- }
+ implicit val serdes = new RootJsonFormat[UUID] {
+ def write(u: UUID) = u.toJson
+
+ def read(value: JsValue) =
+ Try {
+ val JsString(u) = value
+ UUID(u)
+ } getOrElse deserializationError("uuid malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
index f788ac4..c90367e 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
@@ -23,7 +23,7 @@ import java.util.Base64
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
-import scala.util.{ Try, Success, Failure }
+import scala.util.{Failure, Success, Try}
import spray.json._
import spray.json.DefaultJsonProtocol._
@@ -46,57 +46,58 @@ case class ActionLimitsOption(timeout: Option[TimeLimit], memory: Option[MemoryL
* also replaces limits with an optional counterpart for convenience of
* overriding only one value at a time.
*/
-case class WhiskActionPut(
- exec: Option[Exec] = None,
- parameters: Option[Parameters] = None,
- limits: Option[ActionLimitsOption] = None,
- version: Option[SemVer] = None,
- publish: Option[Boolean] = None,
- annotations: Option[Parameters] = None) {
-
- protected[core] def replace(exec: Exec) = {
- WhiskActionPut(Some(exec), parameters, limits, version, publish, annotations)
- }
-
- /**
- * Resolves sequence components if they contain default namespace.
- */
- protected[core] def resolve(userNamespace: EntityName): WhiskActionPut = {
- exec map {
- case SequenceExec(components) =>
- val newExec = SequenceExec(components map {
- c => FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)
- })
- WhiskActionPut(Some(newExec), parameters, limits, version, publish, annotations)
- case _ => this
- } getOrElse this
- }
+case class WhiskActionPut(exec: Option[Exec] = None,
+ parameters: Option[Parameters] = None,
+ limits: Option[ActionLimitsOption] = None,
+ version: Option[SemVer] = None,
+ publish: Option[Boolean] = None,
+ annotations: Option[Parameters] = None) {
+
+ protected[core] def replace(exec: Exec) = {
+ WhiskActionPut(Some(exec), parameters, limits, version, publish, annotations)
+ }
+
+ /**
+ * Resolves sequence components if they contain default namespace.
+ */
+ protected[core] def resolve(userNamespace: EntityName): WhiskActionPut = {
+ exec map {
+ case SequenceExec(components) =>
+ val newExec = SequenceExec(components map { c =>
+ FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)
+ })
+ WhiskActionPut(Some(newExec), parameters, limits, version, publish, annotations)
+ case _ => this
+ } getOrElse this
+ }
}
abstract class WhiskActionLike(override val name: EntityName) extends WhiskEntity(name) {
- def exec: Exec
- def parameters: Parameters
- def limits: ActionLimits
-
- /** @return true iff action has appropriate annotation. */
- def hasFinalParamsAnnotation = {
- annotations.asBool(WhiskAction.finalParamsAnnotationName) getOrElse false
- }
-
- /** @return a Set of immutable parameternames */
- def immutableParameters = if (hasFinalParamsAnnotation) {
- parameters.definedParameters
+ def exec: Exec
+ def parameters: Parameters
+ def limits: ActionLimits
+
+ /** @return true iff action has appropriate annotation. */
+ def hasFinalParamsAnnotation = {
+ annotations.asBool(WhiskAction.finalParamsAnnotationName) getOrElse false
+ }
+
+ /** @return a Set of immutable parameternames */
+ def immutableParameters =
+ if (hasFinalParamsAnnotation) {
+ parameters.definedParameters
} else Set.empty[String]
- def toJson = JsObject(
- "namespace" -> namespace.toJson,
- "name" -> name.toJson,
- "exec" -> exec.toJson,
- "parameters" -> parameters.toJson,
- "limits" -> limits.toJson,
- "version" -> version.toJson,
- "publish" -> publish.toJson,
- "annotations" -> annotations.toJson)
+ def toJson =
+ JsObject(
+ "namespace" -> namespace.toJson,
+ "name" -> name.toJson,
+ "exec" -> exec.toJson,
+ "parameters" -> parameters.toJson,
+ "limits" -> limits.toJson,
+ "version" -> version.toJson,
+ "publish" -> publish.toJson,
+ "annotations" -> annotations.toJson)
}
/**
@@ -117,46 +118,47 @@ abstract class WhiskActionLike(override val name: EntityName) extends WhiskEntit
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
-case class WhiskAction(
- namespace: EntityPath,
- override val name: EntityName,
- exec: Exec,
- parameters: Parameters = Parameters(),
- limits: ActionLimits = ActionLimits(),
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters())
+case class WhiskAction(namespace: EntityPath,
+ override val name: EntityName,
+ exec: Exec,
+ parameters: Parameters = Parameters(),
+ limits: ActionLimits = ActionLimits(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters())
extends WhiskActionLike(name) {
- require(exec != null, "exec undefined")
- require(limits != null, "limits undefined")
-
- /**
- * Merges parameters (usually from package) with existing action parameters.
- * Existing parameters supersede those in p.
- */
- def inherit(p: Parameters) = copy(parameters = p ++ parameters).revision[WhiskAction](rev)
-
- /**
- * Resolves sequence components if they contain default namespace.
- */
- protected[core] def resolve(userNamespace: EntityName): WhiskAction = {
- exec match {
- case SequenceExec(components) =>
- val newExec = SequenceExec(components map {
- c => FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)
- })
- copy(exec = newExec).revision[WhiskAction](rev)
- case _ => this
- }
- }
-
- def toExecutableWhiskAction = exec match {
- case codeExec: CodeExec[_] =>
- Some(ExecutableWhiskAction(namespace, name, codeExec, parameters, limits, version, publish, annotations).revision[ExecutableWhiskAction](rev))
- case _ =>
- None
+ require(exec != null, "exec undefined")
+ require(limits != null, "limits undefined")
+
+ /**
+ * Merges parameters (usually from package) with existing action parameters.
+ * Existing parameters supersede those in p.
+ */
+ def inherit(p: Parameters) = copy(parameters = p ++ parameters).revision[WhiskAction](rev)
+
+ /**
+ * Resolves sequence components if they contain default namespace.
+ */
+ protected[core] def resolve(userNamespace: EntityName): WhiskAction = {
+ exec match {
+ case SequenceExec(components) =>
+ val newExec = SequenceExec(components map { c =>
+ FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)
+ })
+ copy(exec = newExec).revision[WhiskAction](rev)
+ case _ => this
}
+ }
+
+ def toExecutableWhiskAction = exec match {
+ case codeExec: CodeExec[_] =>
+ Some(
+ ExecutableWhiskAction(namespace, name, codeExec, parameters, limits, version, publish, annotations)
+ .revision[ExecutableWhiskAction](rev))
+ case _ =>
+ None
+ }
}
/**
@@ -176,163 +178,174 @@ case class WhiskAction(
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
-case class ExecutableWhiskAction(
- namespace: EntityPath,
- override val name: EntityName,
- exec: CodeExec[_],
- parameters: Parameters = Parameters(),
- limits: ActionLimits = ActionLimits(),
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters())
+case class ExecutableWhiskAction(namespace: EntityPath,
+ override val name: EntityName,
+ exec: CodeExec[_],
+ parameters: Parameters = Parameters(),
+ limits: ActionLimits = ActionLimits(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters())
extends WhiskActionLike(name) {
- require(exec != null, "exec undefined")
- require(limits != null, "limits undefined")
-
- /**
- * Gets initializer for action. This typically includes the code to execute,
- * or a zip file containing the executable artifacts.
- */
- def containerInitializer: JsObject = {
- val code = Option(exec.codeAsJson).filter(_ != JsNull).map("code" -> _)
- val base = Map("name" -> name.toJson, "binary" -> exec.binary.toJson, "main" -> exec.entryPoint.getOrElse("main").toJson)
- JsObject(base ++ code)
- }
-
- def toWhiskAction = WhiskAction(namespace, name, exec, parameters, limits, version, publish, annotations).revision[WhiskAction](rev)
+ require(exec != null, "exec undefined")
+ require(limits != null, "limits undefined")
+
+ /**
+ * Gets initializer for action. This typically includes the code to execute,
+ * or a zip file containing the executable artifacts.
+ */
+ def containerInitializer: JsObject = {
+ val code = Option(exec.codeAsJson).filter(_ != JsNull).map("code" -> _)
+ val base =
+ Map("name" -> name.toJson, "binary" -> exec.binary.toJson, "main" -> exec.entryPoint.getOrElse("main").toJson)
+ JsObject(base ++ code)
+ }
+
+ def toWhiskAction =
+ WhiskAction(namespace, name, exec, parameters, limits, version, publish, annotations).revision[WhiskAction](rev)
}
-object WhiskAction
- extends DocumentFactory[WhiskAction]
- with WhiskEntityQueries[WhiskAction]
- with DefaultJsonProtocol {
+object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {
- val execFieldName = "exec"
- val finalParamsAnnotationName = "final"
+ val execFieldName = "exec"
+ val finalParamsAnnotationName = "final"
- override val collectionName = "actions"
+ override val collectionName = "actions"
- override implicit val serdes = jsonFormat(WhiskAction.apply, "namespace", "name", "exec", "parameters", "limits", "version", "publish", "annotations")
+ override implicit val serdes = jsonFormat(
+ WhiskAction.apply,
+ "namespace",
+ "name",
+ "exec",
+ "parameters",
+ "limits",
+ "version",
+ "publish",
+ "annotations")
- override val cacheEnabled = true
+ override val cacheEnabled = true
- // overriden to store attached code
- override def put[A >: WhiskAction](db: ArtifactStore[A], doc: WhiskAction)(
- implicit transid: TransactionId, notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
+ // overriden to store attached code
+ override def put[A >: WhiskAction](db: ArtifactStore[A], doc: WhiskAction)(
+ implicit transid: TransactionId,
+ notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
- Try {
- require(db != null, "db undefined")
- require(doc != null, "doc undefined")
- } map { _ =>
- doc.exec match {
- case exec @ CodeExecAsAttachment(_, Inline(code), _) =>
- implicit val logger = db.logging
- implicit val ec = db.executionContext
+ Try {
+ require(db != null, "db undefined")
+ require(doc != null, "doc undefined")
+ } map { _ =>
+ doc.exec match {
+ case exec @ CodeExecAsAttachment(_, Inline(code), _) =>
+ implicit val logger = db.logging
+ implicit val ec = db.executionContext
- val newDoc = doc.copy(exec = exec.attach)
- newDoc.revision(doc.rev)
+ val newDoc = doc.copy(exec = exec.attach)
+ newDoc.revision(doc.rev)
- val stream = new ByteArrayInputStream(Base64.getDecoder().decode(code))
- val manifest = exec.manifest.attached.get
+ val stream = new ByteArrayInputStream(Base64.getDecoder().decode(code))
+ val manifest = exec.manifest.attached.get
- for (
- i1 <- super.put(db, newDoc);
- i2 <- attach[A](db, i1, manifest.attachmentName, manifest.attachmentType, stream)
- ) yield i2
+ for (i1 <- super.put(db, newDoc);
+ i2 <- attach[A](db, i1, manifest.attachmentName, manifest.attachmentType, stream)) yield i2
- case _ =>
- super.put(db, doc)
- }
- } match {
- case Success(f) => f
- case Failure(f) => Future.failed(f)
- }
+ case _ =>
+ super.put(db, doc)
+ }
+ } match {
+ case Success(f) => f
+ case Failure(f) => Future.failed(f)
}
+ }
- // overriden to retrieve attached code
- override def get[A >: WhiskAction](db: ArtifactStore[A], doc: DocId, rev: DocRevision = DocRevision.empty, fromCache: Boolean)(
- implicit transid: TransactionId, mw: Manifest[WhiskAction]): Future[WhiskAction] = {
+ // overriden to retrieve attached code
+ override def get[A >: WhiskAction](
+ db: ArtifactStore[A],
+ doc: DocId,
+ rev: DocRevision = DocRevision.empty,
+ fromCache: Boolean)(implicit transid: TransactionId, mw: Manifest[WhiskAction]): Future[WhiskAction] = {
- implicit val ec = db.executionContext
+ implicit val ec = db.executionContext
- val fa = super.get(db, doc, rev, fromCache)
+ val fa = super.get(db, doc, rev, fromCache)
- fa.flatMap { action =>
- action.exec match {
- case exec @ CodeExecAsAttachment(_, Attached(attachmentName, _), _) =>
- val boas = new ByteArrayOutputStream()
- val b64s = Base64.getEncoder().wrap(boas)
+ fa.flatMap { action =>
+ action.exec match {
+ case exec @ CodeExecAsAttachment(_, Attached(attachmentName, _), _) =>
+ val boas = new ByteArrayOutputStream()
+ val b64s = Base64.getEncoder().wrap(boas)
- getAttachment[A](db, action.docinfo, attachmentName, b64s).map { _ =>
- b64s.close()
- val newAction = action.copy(exec = exec.inline(boas.toByteArray))
- newAction.revision(action.rev)
- newAction
- }
+ getAttachment[A](db, action.docinfo, attachmentName, b64s).map { _ =>
+ b64s.close()
+ val newAction = action.copy(exec = exec.inline(boas.toByteArray))
+ newAction.revision(action.rev)
+ newAction
+ }
- case _ =>
- Future.successful(action)
- }
- }
+ case _ =>
+ Future.successful(action)
+ }
}
-
- /**
- * Resolves an action name if it is contained in a package.
- * Look up the package to determine if it is a binding or the actual package.
- * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
- * If it's the actual package, use its name directly as the package path name.
- */
- def resolveAction(db: EntityStore, fullyQualifiedActionName: FullyQualifiedEntityName)(
- implicit ec: ExecutionContext, transid: TransactionId): Future[FullyQualifiedEntityName] = {
- // first check that there is a package to be resolved
- val entityPath = fullyQualifiedActionName.path
- if (entityPath.defaultPackage) {
- // this is the default package, nothing to resolve
- Future.successful(fullyQualifiedActionName)
- } else {
- // there is a package to be resolved
- val pkgDocId = fullyQualifiedActionName.path.toDocId
- val actionName = fullyQualifiedActionName.name
- WhiskPackage.resolveBinding(db, pkgDocId) map {
- _.fullyQualifiedName(withVersion = false).add(actionName)
- }
- }
+ }
+
+ /**
+ * Resolves an action name if it is contained in a package.
+ * Look up the package to determine if it is a binding or the actual package.
+ * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
+ * If it's the actual package, use its name directly as the package path name.
+ */
+ def resolveAction(db: EntityStore, fullyQualifiedActionName: FullyQualifiedEntityName)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId): Future[FullyQualifiedEntityName] = {
+ // first check that there is a package to be resolved
+ val entityPath = fullyQualifiedActionName.path
+ if (entityPath.defaultPackage) {
+ // this is the default package, nothing to resolve
+ Future.successful(fullyQualifiedActionName)
+ } else {
+ // there is a package to be resolved
+ val pkgDocId = fullyQualifiedActionName.path.toDocId
+ val actionName = fullyQualifiedActionName.name
+ WhiskPackage.resolveBinding(db, pkgDocId) map {
+ _.fullyQualifiedName(withVersion = false).add(actionName)
+ }
}
-
- /**
- * Resolves an action name if it is contained in a package.
- * Look up the package to determine if it is a binding or the actual package.
- * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
- * If it's the actual package, use its name directly as the package path name.
- * While traversing the package bindings, merge the parameters.
- */
- def resolveActionAndMergeParameters(entityStore: EntityStore, fullyQualifiedName: FullyQualifiedEntityName)(
- implicit ec: ExecutionContext, transid: TransactionId): Future[WhiskAction] = {
- // first check that there is a package to be resolved
- val entityPath = fullyQualifiedName.path
- if (entityPath.defaultPackage) {
- // this is the default package, nothing to resolve
- WhiskAction.get(entityStore, fullyQualifiedName.toDocId)
- } else {
- // there is a package to be resolved
- val pkgDocid = fullyQualifiedName.path.toDocId
- val actionName = fullyQualifiedName.name
- val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid, mergeParameters = true)
- wp flatMap { resolvedPkg =>
- // fully resolved name for the action
- val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)
- // get the whisk action associate with it and inherit the parameters from the package/binding
- WhiskAction.get(entityStore, fqnAction.toDocId) map { _.inherit(resolvedPkg.parameters) }
- }
- }
+ }
+
+ /**
+ * Resolves an action name if it is contained in a package.
+ * Look up the package to determine if it is a binding or the actual package.
+ * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
+ * If it's the actual package, use its name directly as the package path name.
+ * While traversing the package bindings, merge the parameters.
+ */
+ def resolveActionAndMergeParameters(entityStore: EntityStore, fullyQualifiedName: FullyQualifiedEntityName)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId): Future[WhiskAction] = {
+ // first check that there is a package to be resolved
+ val entityPath = fullyQualifiedName.path
+ if (entityPath.defaultPackage) {
+ // this is the default package, nothing to resolve
+ WhiskAction.get(entityStore, fullyQualifiedName.toDocId)
+ } else {
+ // there is a package to be resolved
+ val pkgDocid = fullyQualifiedName.path.toDocId
+ val actionName = fullyQualifiedName.name
+ val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid, mergeParameters = true)
+ wp flatMap { resolvedPkg =>
+ // fully resolved name for the action
+ val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)
+ // get the whisk action associate with it and inherit the parameters from the package/binding
+ WhiskAction.get(entityStore, fqnAction.toDocId) map { _.inherit(resolvedPkg.parameters) }
+ }
}
+ }
}
object ActionLimitsOption extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat3(ActionLimitsOption.apply)
+ implicit val serdes = jsonFormat3(ActionLimitsOption.apply)
}
object WhiskActionPut extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat6(WhiskActionPut.apply)
+ implicit val serdes = jsonFormat6(WhiskActionPut.apply)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskActivation.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskActivation.scala
index 1904fe1..60af0fd 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskActivation.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskActivation.scala
@@ -49,50 +49,50 @@ import whisk.core.database.DocumentFactory
* @throws IllegalArgumentException if any required argument is undefined
*/
@throws[IllegalArgumentException]
-case class WhiskActivation(
- namespace: EntityPath,
- override val name: EntityName,
- subject: Subject,
- activationId: ActivationId,
- start: Instant,
- end: Instant,
- cause: Option[ActivationId] = None,
- response: ActivationResponse = ActivationResponse.success(),
- logs: ActivationLogs = ActivationLogs(),
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters(),
- duration: Option[Long] = None)
+case class WhiskActivation(namespace: EntityPath,
+ override val name: EntityName,
+ subject: Subject,
+ activationId: ActivationId,
+ start: Instant,
+ end: Instant,
+ cause: Option[ActivationId] = None,
+ response: ActivationResponse = ActivationResponse.success(),
+ logs: ActivationLogs = ActivationLogs(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters(),
+ duration: Option[Long] = None)
extends WhiskEntity(EntityName(activationId.asString)) {
- require(cause != null, "cause undefined")
- require(start != null, "start undefined")
- require(end != null, "end undefined")
- require(response != null, "response undefined")
+ require(cause != null, "cause undefined")
+ require(start != null, "start undefined")
+ require(end != null, "end undefined")
+ require(response != null, "response undefined")
- def toJson = WhiskActivation.serdes.write(this).asJsObject
+ def toJson = WhiskActivation.serdes.write(this).asJsObject
- override def summaryAsJson = {
- val JsObject(fields) = super.summaryAsJson
- JsObject(fields + ("activationId" -> activationId.toJson))
- }
+ override def summaryAsJson = {
+ val JsObject(fields) = super.summaryAsJson
+ JsObject(fields + ("activationId" -> activationId.toJson))
+ }
- def resultAsJson = response.result.toJson.asJsObject
+ def resultAsJson = response.result.toJson.asJsObject
- def toExtendedJson = {
- val JsObject(baseFields) = WhiskActivation.serdes.write(this).asJsObject
- val newFields = (baseFields - "response") + ("response" -> response.toExtendedJson)
- if (end != Instant.EPOCH) {
- val durationValue = (duration getOrElse (end.toEpochMilli - start.toEpochMilli)).toJson
- JsObject(newFields + ("duration" -> durationValue))
- } else {
- JsObject(newFields - "end")
- }
+ def toExtendedJson = {
+ val JsObject(baseFields) = WhiskActivation.serdes.write(this).asJsObject
+ val newFields = (baseFields - "response") + ("response" -> response.toExtendedJson)
+ if (end != Instant.EPOCH) {
+ val durationValue = (duration getOrElse (end.toEpochMilli - start.toEpochMilli)).toJson
+ JsObject(newFields + ("duration" -> durationValue))
+ } else {
+ JsObject(newFields - "end")
}
+ }
- def withoutLogsOrResult = copy(response = response.withoutResult, logs = ActivationLogs()).revision[WhiskActivation](rev)
- def withoutLogs = copy(logs = ActivationLogs()).revision[WhiskActivation](rev)
- def withLogs(logs: ActivationLogs) = copy(logs = logs).revision[WhiskActivation](rev)
+ def withoutLogsOrResult =
+ copy(response = response.withoutResult, logs = ActivationLogs()).revision[WhiskActivation](rev)
+ def withoutLogs = copy(logs = ActivationLogs()).revision[WhiskActivation](rev)
+ def withLogs(logs: ActivationLogs) = copy(logs = logs).revision[WhiskActivation](rev)
}
object WhiskActivation
@@ -100,22 +100,23 @@ object WhiskActivation
with WhiskEntityQueries[WhiskActivation]
with DefaultJsonProtocol {
- private implicit val instantSerdes = new RootJsonFormat[Instant] {
- def write(t: Instant) = t.toEpochMilli.toJson
+ private implicit val instantSerdes = new RootJsonFormat[Instant] {
+ def write(t: Instant) = t.toEpochMilli.toJson
- def read(value: JsValue) = Try {
- value match {
- case JsString(t) => Instant.parse(t)
- case JsNumber(i) => Instant.ofEpochMilli(i.bigDecimal.longValue)
- case _ => deserializationError("timetsamp malformed")
- }
- } getOrElse deserializationError("timetsamp malformed 2")
- }
+ def read(value: JsValue) =
+ Try {
+ value match {
+ case JsString(t) => Instant.parse(t)
+ case JsNumber(i) => Instant.ofEpochMilli(i.bigDecimal.longValue)
+ case _ => deserializationError("timetsamp malformed")
+ }
+ } getOrElse deserializationError("timetsamp malformed 2")
+ }
- override val collectionName = "activations"
- override implicit val serdes = jsonFormat13(WhiskActivation.apply)
+ override val collectionName = "activations"
+ override implicit val serdes = jsonFormat13(WhiskActivation.apply)
- // Caching activations doesn't make much sense in the common case as usually,
- // an activation is only asked for once.
- override val cacheEnabled = false
+ // Caching activations doesn't make much sense in the common case as usually,
+ // an activation is only asked for once.
+ override val cacheEnabled = false
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskAuth.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskAuth.scala
index e464ce0..be83471 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskAuth.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskAuth.scala
@@ -28,19 +28,18 @@ import scala.util.Try
protected[core] case class WhiskNamespace(name: EntityName, authkey: AuthKey)
protected[core] object WhiskNamespace extends DefaultJsonProtocol {
- implicit val serdes = new RootJsonFormat[WhiskNamespace] {
- def write(w: WhiskNamespace) = JsObject(
- "name" -> w.name.toJson,
- "uuid" -> w.authkey.uuid.toJson,
- "key" -> w.authkey.key.toJson)
-
- def read(value: JsValue) = Try {
- value.asJsObject.getFields("name", "uuid", "key") match {
- case Seq(JsString(n), JsString(u), JsString(k)) =>
- WhiskNamespace(EntityName(n), AuthKey(UUID(u), Secret(k)))
- }
- } getOrElse deserializationError("namespace record malformed")
- }
+ implicit val serdes = new RootJsonFormat[WhiskNamespace] {
+ def write(w: WhiskNamespace) =
+ JsObject("name" -> w.name.toJson, "uuid" -> w.authkey.uuid.toJson, "key" -> w.authkey.key.toJson)
+
+ def read(value: JsValue) =
+ Try {
+ value.asJsObject.getFields("name", "uuid", "key") match {
+ case Seq(JsString(n), JsString(u), JsString(k)) =>
+ WhiskNamespace(EntityName(n), AuthKey(UUID(u), Secret(k)))
+ }
+ } getOrElse deserializationError("namespace record malformed")
+ }
}
/**
@@ -48,20 +47,15 @@ protected[core] object WhiskNamespace extends DefaultJsonProtocol {
* top-level authkey is given but each subject has a set of namespaces,
* which in turn have the keys.
*/
-protected[core] case class WhiskAuth(
- subject: Subject,
- namespaces: Set[WhiskNamespace])
- extends WhiskDocument {
+protected[core] case class WhiskAuth(subject: Subject, namespaces: Set[WhiskNamespace]) extends WhiskDocument {
- override def docid = DocId(subject.asString)
+ override def docid = DocId(subject.asString)
- def toJson = JsObject(
- "subject" -> subject.toJson,
- "namespaces" -> namespaces.toJson)
+ def toJson = JsObject("subject" -> subject.toJson, "namespaces" -> namespaces.toJson)
}
protected[core] object WhiskAuth extends DefaultJsonProtocol {
- // Need to explicitly set field names since WhiskAuth extends WhiskDocument
- // which defines more than the 2 "standard" fields
- implicit val serdes = jsonFormat(WhiskAuth.apply, "subject", "namespaces")
+ // Need to explicitly set field names since WhiskAuth extends WhiskDocument
+ // which defines more than the 2 "standard" fields
+ implicit val serdes = jsonFormat(WhiskAuth.apply, "subject", "namespaces")
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
index 224f2c2..166b4c8 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
@@ -43,66 +43,67 @@ import whisk.http.Messages
@throws[IllegalArgumentException]
abstract class WhiskEntity protected[entity] (en: EntityName) extends WhiskDocument {
- val namespace: EntityPath
- val name = en
- val version: SemVer
- val publish: Boolean
- val annotations: Parameters
- val updated = Instant.now(Clock.systemUTC())
-
- /**
- * The name of the entity qualified with its namespace and version for
- * creating unique keys in backend services.
- */
- final def fullyQualifiedName(withVersion: Boolean) = FullyQualifiedEntityName(namespace, en, if (withVersion) Some(version) else None)
-
- /** The primary key for the entity in the datastore */
- override final def docid = fullyQualifiedName(false).toDocId
-
- /**
- * Returns a JSON object with the fields specific to this abstract class.
- */
- protected def entityDocumentRecord: JsObject = JsObject(
- "name" -> JsString(name.toString),
- "updated" -> JsNumber(updated.toEpochMilli()))
-
- override def toDocumentRecord: JsObject = {
- val extraFields = entityDocumentRecord.fields
- val base = super.toDocumentRecord
-
- // In this order to make sure the subclass can rewrite using toJson.
- JsObject(extraFields ++ base.fields)
- }
-
- /**
- * @return the primary key (name) of the entity as a pithy description
- */
- override def toString = s"${this.getClass.getSimpleName}/${fullyQualifiedName(true)}"
-
- /**
- * A JSON view of the entity, that should match the result returned in a list operation.
- * This should be synchronized with the views computed in wipeTransientDBs.sh.
- */
- def summaryAsJson = JsObject(
- "namespace" -> namespace.toJson,
- "name" -> name.toJson,
- "version" -> version.toJson,
- WhiskEntity.sharedFieldName -> JsBoolean(publish),
- "annotations" -> annotations.toJsArray)
+ val namespace: EntityPath
+ val name = en
+ val version: SemVer
+ val publish: Boolean
+ val annotations: Parameters
+ val updated = Instant.now(Clock.systemUTC())
+
+ /**
+ * The name of the entity qualified with its namespace and version for
+ * creating unique keys in backend services.
+ */
+ final def fullyQualifiedName(withVersion: Boolean) =
+ FullyQualifiedEntityName(namespace, en, if (withVersion) Some(version) else None)
+
+ /** The primary key for the entity in the datastore */
+ override final def docid = fullyQualifiedName(false).toDocId
+
+ /**
+ * Returns a JSON object with the fields specific to this abstract class.
+ */
+ protected def entityDocumentRecord: JsObject =
+ JsObject("name" -> JsString(name.toString), "updated" -> JsNumber(updated.toEpochMilli()))
+
+ override def toDocumentRecord: JsObject = {
+ val extraFields = entityDocumentRecord.fields
+ val base = super.toDocumentRecord
+
+ // In this order to make sure the subclass can rewrite using toJson.
+ JsObject(extraFields ++ base.fields)
+ }
+
+ /**
+ * @return the primary key (name) of the entity as a pithy description
+ */
+ override def toString = s"${this.getClass.getSimpleName}/${fullyQualifiedName(true)}"
+
+ /**
+ * A JSON view of the entity, that should match the result returned in a list operation.
+ * This should be synchronized with the views computed in wipeTransientDBs.sh.
+ */
+ def summaryAsJson =
+ JsObject(
+ "namespace" -> namespace.toJson,
+ "name" -> name.toJson,
+ "version" -> version.toJson,
+ WhiskEntity.sharedFieldName -> JsBoolean(publish),
+ "annotations" -> annotations.toJsArray)
}
object WhiskEntity {
- val sharedFieldName = "publish"
- val paramsFieldName = "parameters"
- val annotationsFieldName = "annotations"
+ val sharedFieldName = "publish"
+ val paramsFieldName = "parameters"
+ val annotationsFieldName = "annotations"
- /**
- * Gets fully qualified name of an activation based on its namespace and activation id.
- */
- def qualifiedName(namespace: EntityPath, activationId: ActivationId) = {
- s"$namespace${EntityPath.PATHSEP}$activationId"
- }
+ /**
+ * Gets fully qualified name of an activation based on its namespace and activation id.
+ */
+ def qualifiedName(namespace: EntityPath, activationId: ActivationId) = {
+ s"$namespace${EntityPath.PATHSEP}$activationId"
+ }
}
/**
@@ -110,73 +111,80 @@ object WhiskEntity {
* avoid multiple implicit alternatives when working with one of the subtypes.
*/
object WhiskEntityJsonFormat extends RootJsonFormat[WhiskEntity] {
- // THE ORDER MATTERS! E.g. some triggers can deserialize as packages, but not
- // the other way around. Try most specific first!
- private def readers: Stream[JsValue => WhiskEntity] = Stream(
- WhiskAction.serdes.read,
- WhiskActivation.serdes.read,
- WhiskRule.serdes.read,
- WhiskTrigger.serdes.read,
- WhiskPackage.serdes.read)
-
- // Not necessarily the smartest way to go about this. In theory, whenever
- // a more precise type is known, this method shouldn't be used.
- override def read(js: JsValue): WhiskEntity = {
- val successes: Stream[WhiskEntity] = readers.flatMap(r => Try(r(js)).toOption)
- successes.headOption.getOrElse {
- throw DocumentUnreadable(Messages.corruptedEntity)
- }
- }
-
- override def write(we: WhiskEntity): JsValue = we match {
- case a: WhiskAction => WhiskAction.serdes.write(a)
- case a: WhiskActivation => WhiskActivation.serdes.write(a)
- case p: WhiskPackage => WhiskPackage.serdes.write(p)
- case r: WhiskRule => WhiskRule.serdes.write(r)
- case t: WhiskTrigger => WhiskTrigger.serdes.write(t)
+ // THE ORDER MATTERS! E.g. some triggers can deserialize as packages, but not
+ // the other way around. Try most specific first!
+ private def readers: Stream[JsValue => WhiskEntity] =
+ Stream(
+ WhiskAction.serdes.read,
+ WhiskActivation.serdes.read,
+ WhiskRule.serdes.read,
+ WhiskTrigger.serdes.read,
+ WhiskPackage.serdes.read)
+
+ // Not necessarily the smartest way to go about this. In theory, whenever
+ // a more precise type is known, this method shouldn't be used.
+ override def read(js: JsValue): WhiskEntity = {
+ val successes: Stream[WhiskEntity] = readers.flatMap(r => Try(r(js)).toOption)
+ successes.headOption.getOrElse {
+ throw DocumentUnreadable(Messages.corruptedEntity)
}
+ }
+
+ override def write(we: WhiskEntity): JsValue = we match {
+ case a: WhiskAction => WhiskAction.serdes.write(a)
+ case a: WhiskActivation => WhiskActivation.serdes.write(a)
+ case p: WhiskPackage => WhiskPackage.serdes.write(p)
+ case r: WhiskRule => WhiskRule.serdes.write(r)
+ case t: WhiskTrigger => WhiskTrigger.serdes.write(t)
+ }
}
/**
* Trait for the objects we want to size. The size will be defined as ByteSize.
*/
trait ByteSizeable {
- /**
- * Method to calculate the size of the object.
- * The size of the object is defined as the sum of sizes of all parameters, that is stored in the object.
- *
- * @return the size of the object as ByteSize
- */
- def size: ByteSize
+
+ /**
+ * Method to calculate the size of the object.
+ * The size of the object is defined as the sum of sizes of all parameters, that is stored in the object.
+ *
+ * @return the size of the object as ByteSize
+ */
+ def size: ByteSize
}
object LimitedWhiskEntityPut extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat3(LimitedWhiskEntityPut.apply)
+ implicit val serdes = jsonFormat3(LimitedWhiskEntityPut.apply)
}
case class SizeError(field: String, is: ByteSize, allowed: ByteSize)
-case class LimitedWhiskEntityPut(
- exec: Option[Exec] = None,
- parameters: Option[Parameters] = None,
- annotations: Option[Parameters] = None) {
-
- def isWithinSizeLimits: Option[SizeError] = {
- exec.flatMap { e =>
- val is = e.size
- if (is <= Exec.sizeLimit) None else Some {
- SizeError(WhiskAction.execFieldName, is, Exec.sizeLimit)
- }
- } orElse parameters.flatMap { p =>
- val is = p.size
- if (is <= Parameters.sizeLimit) None else Some {
- SizeError(WhiskEntity.paramsFieldName, is, Parameters.sizeLimit)
- }
- } orElse annotations.flatMap { a =>
- val is = a.size
- if (is <= Parameters.sizeLimit) None else Some {
- SizeError(WhiskEntity.annotationsFieldName, is, Parameters.sizeLimit)
- }
+case class LimitedWhiskEntityPut(exec: Option[Exec] = None,
+ parameters: Option[Parameters] = None,
+ annotations: Option[Parameters] = None) {
+
+ def isWithinSizeLimits: Option[SizeError] = {
+ exec.flatMap { e =>
+ val is = e.size
+ if (is <= Exec.sizeLimit) None
+ else
+ Some {
+ SizeError(WhiskAction.execFieldName, is, Exec.sizeLimit)
+ }
+ } orElse parameters.flatMap { p =>
+ val is = p.size
+ if (is <= Parameters.sizeLimit) None
+ else
+ Some {
+ SizeError(WhiskEntity.paramsFieldName, is, Parameters.sizeLimit)
+ }
+ } orElse annotations.flatMap { a =>
+ val is = a.size
+ if (is <= Parameters.sizeLimit) None
+ else
+ Some {
+ SizeError(WhiskEntity.annotationsFieldName, is, Parameters.sizeLimit)
}
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskPackage.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskPackage.scala
index ac73147..b9c1c46 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskPackage.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskPackage.scala
@@ -33,19 +33,18 @@ import whisk.core.entity.types.EntityStore
* WhiskPackagePut is a restricted WhiskPackage view that eschews properties
* that are auto-assigned or derived from URI: namespace and name.
*/
-case class WhiskPackagePut(
- binding: Option[Binding] = None,
- parameters: Option[Parameters] = None,
- version: Option[SemVer] = None,
- publish: Option[Boolean] = None,
- annotations: Option[Parameters] = None) {
-
- /**
- * Resolves the binding if it contains the default namespace.
- */
- protected[core] def resolve(namespace: EntityName): WhiskPackagePut = {
- WhiskPackagePut(binding.map(_.resolve(namespace)), parameters, version, publish, annotations)
- }
+case class WhiskPackagePut(binding: Option[Binding] = None,
+ parameters: Option[Parameters] = None,
+ version: Option[SemVer] = None,
+ publish: Option[Boolean] = None,
+ annotations: Option[Parameters] = None) {
+
+ /**
+ * Resolves the binding if it contains the default namespace.
+ */
+ protected[core] def resolve(namespace: EntityName): WhiskPackagePut = {
+ WhiskPackagePut(binding.map(_.resolve(namespace)), parameters, version, publish, annotations)
+ }
}
/**
@@ -65,80 +64,83 @@ case class WhiskPackagePut(
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
-case class WhiskPackage(
- namespace: EntityPath,
- override val name: EntityName,
- binding: Option[Binding] = None,
- parameters: Parameters = Parameters(),
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters())
+case class WhiskPackage(namespace: EntityPath,
+ override val name: EntityName,
+ binding: Option[Binding] = None,
+ parameters: Parameters = Parameters(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters())
extends WhiskEntity(name) {
- require(binding != null || (binding map { _ != null } getOrElse true), "binding undefined")
-
- /**
- * Merges parameters into existing set of parameters for package.
- * Existing parameters supersede those in p.
- */
- def inherit(p: Parameters): WhiskPackage = copy(parameters = p ++ parameters).revision[WhiskPackage](rev)
-
- /**
- * Merges parameters into existing set of parameters for package.
- * The parameters from p supersede parameters from this.
- */
- def mergeParameters(p: Parameters): WhiskPackage = copy(parameters = parameters ++ p).revision[WhiskPackage](rev)
-
- /**
- * Gets the full path for the package.
- * This is equivalent to calling this this.fullyQualifiedName(withVersion = false).fullPath.
- */
- def fullPath: EntityPath = namespace.addPath(name)
-
- /**
- * Gets binding for package iff this is not already a package reference.
- */
- def bind: Option[Binding] = {
- if (binding.isDefined) {
- None
- } else {
- Some(Binding(namespace.root, name))
- }
- }
-
- /**
- * Adds actions to package. The actions list is filtered so that only actions that
- * match the package are included (must match package namespace/name).
- */
- def withActions(actions: List[WhiskAction] = List()): WhiskPackageWithActions = {
- withPackageActions(actions filter { a =>
- val pkgns = binding map { b => b.namespace.addPath(b.name) } getOrElse { namespace.addPath(name) }
- a.namespace == pkgns
- } map { a =>
- WhiskPackageAction(a.name, a.version, a.annotations)
- })
- }
-
- /**
- * Adds package actions to package as actions or feeds. An action is considered a feed
- * is it defined the property "feed" in the annotation. The value of the property is ignored
- * for this check.
- */
- def withPackageActions(actions: List[WhiskPackageAction] = List()): WhiskPackageWithActions = {
- val actionGroups = actions map { a =>
- // group into "actions" and "feeds"
- val feed = a.annotations.get(Parameters.Feed) map { _ => true } getOrElse false
- (feed, a)
- } groupBy { _._1 } mapValues { _.map(_._2) }
- WhiskPackageWithActions(this, actionGroups.getOrElse(false, List()), actionGroups.getOrElse(true, List()))
- }
-
- def toJson = WhiskPackage.serdes.write(this).asJsObject
-
- override def summaryAsJson = {
- val JsObject(fields) = super.summaryAsJson
- JsObject(fields + (WhiskPackage.bindingFieldName -> binding.isDefined.toJson))
+ require(binding != null || (binding map { _ != null } getOrElse true), "binding undefined")
+
+ /**
+ * Merges parameters into existing set of parameters for package.
+ * Existing parameters supersede those in p.
+ */
+ def inherit(p: Parameters): WhiskPackage = copy(parameters = p ++ parameters).revision[WhiskPackage](rev)
+
+ /**
+ * Merges parameters into existing set of parameters for package.
+ * The parameters from p supersede parameters from this.
+ */
+ def mergeParameters(p: Parameters): WhiskPackage = copy(parameters = parameters ++ p).revision[WhiskPackage](rev)
+
+ /**
+ * Gets the full path for the package.
+ * This is equivalent to calling this this.fullyQualifiedName(withVersion = false).fullPath.
+ */
+ def fullPath: EntityPath = namespace.addPath(name)
+
+ /**
+ * Gets binding for package iff this is not already a package reference.
+ */
+ def bind: Option[Binding] = {
+ if (binding.isDefined) {
+ None
+ } else {
+ Some(Binding(namespace.root, name))
}
+ }
+
+ /**
+ * Adds actions to package. The actions list is filtered so that only actions that
+ * match the package are included (must match package namespace/name).
+ */
+ def withActions(actions: List[WhiskAction] = List()): WhiskPackageWithActions = {
+ withPackageActions(actions filter { a =>
+ val pkgns = binding map { b =>
+ b.namespace.addPath(b.name)
+ } getOrElse { namespace.addPath(name) }
+ a.namespace == pkgns
+ } map { a =>
+ WhiskPackageAction(a.name, a.version, a.annotations)
+ })
+ }
+
+ /**
+ * Adds package actions to package as actions or feeds. An action is considered a feed
+ * is it defined the property "feed" in the annotation. The value of the property is ignored
+ * for this check.
+ */
+ def withPackageActions(actions: List[WhiskPackageAction] = List()): WhiskPackageWithActions = {
+ val actionGroups = actions map { a =>
+ // group into "actions" and "feeds"
+ val feed = a.annotations.get(Parameters.Feed) map { _ =>
+ true
+ } getOrElse false
+ (feed, a)
+ } groupBy { _._1 } mapValues { _.map(_._2) }
+ WhiskPackageWithActions(this, actionGroups.getOrElse(false, List()), actionGroups.getOrElse(true, List()))
+ }
+
+ def toJson = WhiskPackage.serdes.write(this).asJsObject
+
+ override def summaryAsJson = {
+ val JsObject(fields) = super.summaryAsJson
+ JsObject(fields + (WhiskPackage.bindingFieldName -> binding.isDefined.toJson))
+ }
}
/**
@@ -158,46 +160,48 @@ object WhiskPackage
with WhiskEntityQueries[WhiskPackage]
with DefaultJsonProtocol {
- val bindingFieldName = "binding"
- override val collectionName = "packages"
+ val bindingFieldName = "binding"
+ override val collectionName = "packages"
+
+ /**
+ * Traverses a binding recursively to find the root package and
+ * merges parameters along the way if mergeParameters flag is set.
+ *
+ * @param db the entity store containing packages
+ * @param pkg the package document id to start resolving
+ * @param mergeParameters flag that indicates whether parameters should be merged during package resolution
+ * @return the same package if there is no binding, or the actual reference package otherwise
+ */
+ def resolveBinding(db: EntityStore, pkg: DocId, mergeParameters: Boolean = false)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId): Future[WhiskPackage] = {
+ WhiskPackage.get(db, pkg) flatMap { wp =>
+ // if there is a binding resolve it
+ val resolved = wp.binding map { binding =>
+ if (mergeParameters) {
+ resolveBinding(db, binding.docid, true) map { resolvedPackage =>
+ resolvedPackage.mergeParameters(wp.parameters)
+ }
+ } else resolveBinding(db, binding.docid)
+ }
+ resolved getOrElse Future.successful(wp)
+ }
+ }
+
+ override implicit val serdes = {
/**
- * Traverses a binding recursively to find the root package and
- * merges parameters along the way if mergeParameters flag is set.
- *
- * @param db the entity store containing packages
- * @param pkg the package document id to start resolving
- * @param mergeParameters flag that indicates whether parameters should be merged during package resolution
- * @return the same package if there is no binding, or the actual reference package otherwise
+ * Custom serdes for a binding - this property must be present in the datastore records for
+ * packages so that views can map over packages vs bindings.
*/
- def resolveBinding(db: EntityStore, pkg: DocId, mergeParameters: Boolean = false)(
- implicit ec: ExecutionContext, transid: TransactionId): Future[WhiskPackage] = {
- WhiskPackage.get(db, pkg) flatMap { wp =>
- // if there is a binding resolve it
- val resolved = wp.binding map { binding =>
- if (mergeParameters) {
- resolveBinding(db, binding.docid, true) map {
- resolvedPackage => resolvedPackage.mergeParameters(wp.parameters)
- }
- } else resolveBinding(db, binding.docid)
- }
- resolved getOrElse Future.successful(wp)
- }
- }
-
- override implicit val serdes = {
- /**
- * Custom serdes for a binding - this property must be present in the datastore records for
- * packages so that views can map over packages vs bindings.
- */
- implicit val bindingOverride = new JsonFormat[Option[Binding]] {
- override def write(b: Option[Binding]) = Binding.optionalBindingSerializer.write(b)
- override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)
- }
- jsonFormat7(WhiskPackage.apply)
+ implicit val bindingOverride = new JsonFormat[Option[Binding]] {
+ override def write(b: Option[Binding]) = Binding.optionalBindingSerializer.write(b)
+ override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)
}
+ jsonFormat7(WhiskPackage.apply)
+ }
- override val cacheEnabled = true
+ override val cacheEnabled = true
}
/**
@@ -205,76 +209,81 @@ object WhiskPackage
* namespace and package name.
*/
case class Binding(namespace: EntityName, name: EntityName) {
- def fullyQualifiedName = FullyQualifiedEntityName(namespace.toPath, name)
- def docid = fullyQualifiedName.toDocId
- override def toString = fullyQualifiedName.toString
-
- /**
- * Returns a Binding namespace if it is the default namespace
- * to the given one, otherwise this is an identity.
- */
- def resolve(ns: EntityName): Binding = {
- namespace.toPath match {
- case EntityPath.DEFAULT => Binding(ns, name)
- case _ => this
- }
+ def fullyQualifiedName = FullyQualifiedEntityName(namespace.toPath, name)
+ def docid = fullyQualifiedName.toDocId
+ override def toString = fullyQualifiedName.toString
+
+ /**
+ * Returns a Binding namespace if it is the default namespace
+ * to the given one, otherwise this is an identity.
+ */
+ def resolve(ns: EntityName): Binding = {
+ namespace.toPath match {
+ case EntityPath.DEFAULT => Binding(ns, name)
+ case _ => this
}
+ }
}
object Binding extends ArgNormalizer[Binding] with DefaultJsonProtocol {
- override protected[core] val serdes = jsonFormat2(Binding.apply)
-
- protected[entity] val optionalBindingDeserializer = new JsonReader[Option[Binding]] {
- override def read(js: JsValue) = {
- if (js == JsObject()) None else Some(serdes.read(js))
- }
+ override protected[core] val serdes = jsonFormat2(Binding.apply)
+ protected[entity] val optionalBindingDeserializer = new JsonReader[Option[Binding]] {
+ override def read(js: JsValue) = {
+ if (js == JsObject()) None else Some(serdes.read(js))
}
- protected[entity] val optionalBindingSerializer = new JsonWriter[Option[Binding]] {
- override def write(b: Option[Binding]) = b match {
- case None => JsObject()
- case Some(n) => Binding.serdes.write(n)
- }
+ }
+
+ protected[entity] val optionalBindingSerializer = new JsonWriter[Option[Binding]] {
+ override def write(b: Option[Binding]) = b match {
+ case None => JsObject()
+ case Some(n) => Binding.serdes.write(n)
}
+ }
}
object WhiskPackagePut extends DefaultJsonProtocol {
- implicit val serdes = {
- implicit val bindingSerdes = Binding.serdes
- implicit val optionalBindingSerdes = new OptionFormat[Binding] {
- override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)
- override def write(n: Option[Binding]) = Binding.optionalBindingSerializer.write(n)
- }
- jsonFormat5(WhiskPackagePut.apply)
+ implicit val serdes = {
+ implicit val bindingSerdes = Binding.serdes
+ implicit val optionalBindingSerdes = new OptionFormat[Binding] {
+ override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)
+ override def write(n: Option[Binding]) = Binding.optionalBindingSerializer.write(n)
}
+ jsonFormat5(WhiskPackagePut.apply)
+ }
}
object WhiskPackageAction extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat3(WhiskPackageAction.apply)
+ implicit val serdes = jsonFormat3(WhiskPackageAction.apply)
}
object WhiskPackageWithActions {
- implicit val serdes = new RootJsonFormat[WhiskPackageWithActions] {
- def write(w: WhiskPackageWithActions) = {
- val JsObject(pkg) = WhiskPackage.serdes.write(w.wp)
- JsObject(pkg + ("actions" -> w.actions.toJson) + ("feeds" -> w.feeds.toJson))
- }
-
- def read(value: JsValue) = Try {
- val pkg = WhiskPackage.serdes.read(value)
- val actions = value.asJsObject.getFields("actions") match {
- case Seq(JsArray(as)) =>
- as map { a => WhiskPackageAction.serdes.read(a) } toList
- case _ => List()
- }
- val feeds = value.asJsObject.getFields("feeds") match {
- case Seq(JsArray(as)) =>
- as map { a => WhiskPackageAction.serdes.read(a) } toList
- case _ => List()
- }
- WhiskPackageWithActions(pkg, actions, feeds)
- } getOrElse deserializationError("whisk package with actions malformed")
+ implicit val serdes = new RootJsonFormat[WhiskPackageWithActions] {
+ def write(w: WhiskPackageWithActions) = {
+ val JsObject(pkg) = WhiskPackage.serdes.write(w.wp)
+ JsObject(pkg + ("actions" -> w.actions.toJson) + ("feeds" -> w.feeds.toJson))
}
+
+ def read(value: JsValue) =
+ Try {
+ val pkg = WhiskPackage.serdes.read(value)
+ val actions = value.asJsObject.getFields("actions") match {
+ case Seq(JsArray(as)) =>
+ as map { a =>
+ WhiskPackageAction.serdes.read(a)
+ } toList
+ case _ => List()
+ }
+ val feeds = value.asJsObject.getFields("feeds") match {
+ case Seq(JsArray(as)) =>
+ as map { a =>
+ WhiskPackageAction.serdes.read(a)
+ } toList
+ case _ => List()
+ }
+ WhiskPackageWithActions(pkg, actions, feeds)
+ } getOrElse deserializationError("whisk package with actions malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskRule.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskRule.scala
index 749b6f5..550d1bd 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskRule.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskRule.scala
@@ -34,21 +34,20 @@ import whisk.core.database.DocumentFactory
* WhiskRulePut is a restricted WhiskRule view that eschews properties
* that are auto-assigned or derived from URI: namespace, name, status.
*/
-case class WhiskRulePut(
- trigger: Option[FullyQualifiedEntityName] = None,
- action: Option[FullyQualifiedEntityName] = None,
- version: Option[SemVer] = None,
- publish: Option[Boolean] = None,
- annotations: Option[Parameters] = None) {
-
- /**
- * Resolves the trigger and action name if they contains the default namespace.
- */
- protected[core] def resolve(namespace: EntityName): WhiskRulePut = {
- val t = trigger map { _.resolve(namespace) }
- val a = action map { _.resolve(namespace) }
- WhiskRulePut(t, a, version, publish, annotations)
- }
+case class WhiskRulePut(trigger: Option[FullyQualifiedEntityName] = None,
+ action: Option[FullyQualifiedEntityName] = None,
+ version: Option[SemVer] = None,
+ publish: Option[Boolean] = None,
+ annotations: Option[Parameters] = None) {
+
+ /**
+ * Resolves the trigger and action name if they contains the default namespace.
+ */
+ protected[core] def resolve(namespace: EntityName): WhiskRulePut = {
+ val t = trigger map { _.resolve(namespace) }
+ val a = action map { _.resolve(namespace) }
+ WhiskRulePut(t, a, version, publish, annotations)
+ }
}
/**
@@ -70,19 +69,18 @@ case class WhiskRulePut(
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
-case class WhiskRule(
- namespace: EntityPath,
- override val name: EntityName,
- trigger: FullyQualifiedEntityName,
- action: FullyQualifiedEntityName,
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters())
+case class WhiskRule(namespace: EntityPath,
+ override val name: EntityName,
+ trigger: FullyQualifiedEntityName,
+ action: FullyQualifiedEntityName,
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters())
extends WhiskEntity(name) {
- def withStatus(s: Status) = WhiskRuleResponse(namespace, name, s, trigger, action, version, publish, annotations)
+ def withStatus(s: Status) = WhiskRuleResponse(namespace, name, s, trigger, action, version, publish, annotations)
- def toJson = WhiskRule.serdes.write(this).asJsObject
+ def toJson = WhiskRule.serdes.write(this).asJsObject
}
/**
@@ -99,17 +97,16 @@ case class WhiskRule(
* @param publish true to share the action or false otherwise
* @param annotation the set of annotations to attribute to the rule
*/
-case class WhiskRuleResponse(
- namespace: EntityPath,
- name: EntityName,
- status: Status,
- trigger: FullyQualifiedEntityName,
- action: FullyQualifiedEntityName,
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters()) {
-
- def toWhiskRule = WhiskRule(namespace, name, trigger, action, version, publish, annotations)
+case class WhiskRuleResponse(namespace: EntityPath,
+ name: EntityName,
+ status: Status,
+ trigger: FullyQualifiedEntityName,
+ action: FullyQualifiedEntityName,
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters()) {
+
+ def toWhiskRule = WhiskRule(namespace, name, trigger, action, version, publish, annotations)
}
/**
@@ -130,117 +127,117 @@ case class WhiskRuleResponse(
* @param status, one of allowed status strings
*/
class Status private (private val status: String) extends AnyVal {
- override def toString = status
+ override def toString = status
}
protected[core] object Status extends ArgNormalizer[Status] {
- val ACTIVE = new Status("active")
- val INACTIVE = new Status("inactive")
-
- protected[core] def next(status: Status): Status = {
- status match {
- case ACTIVE => INACTIVE
- case INACTIVE => ACTIVE
- }
- }
-
- /**
- * Creates a rule Status from a string.
- *
- * @param str the rule status as string
- * @return Status instance
- * @throws IllegalArgumentException is argument is undefined or not a valid status
- */
- @throws[IllegalArgumentException]
- override protected[entity] def factory(str: String): Status = {
- val status = new Status(str)
- require(status == ACTIVE || status == INACTIVE,
- s"$str is not a recognized rule state")
- status
- }
-
- override protected[core] implicit val serdes = new RootJsonFormat[Status] {
- def write(s: Status) = JsString(s.status)
+ val ACTIVE = new Status("active")
+ val INACTIVE = new Status("inactive")
- def read(value: JsValue) = Try {
- val JsString(v) = value
- Status(v)
- } match {
- case Success(s) => s
- case Failure(t) => deserializationError(t.getMessage)
- }
- }
-
- /**
- * A serializer for status POST entities. This is a restricted
- * Status view with only ACTIVE and INACTIVE values allowed.
- */
- protected[core] val serdesRestricted = new RootJsonFormat[Status] {
- def write(s: Status) = JsObject("status" -> JsString(s.status))
-
- def read(value: JsValue) = Try {
- val JsObject(fields) = value
- val JsString(s) = fields("status")
- Status(s)
- } match {
- case Success(status) =>
- if (status == ACTIVE || status == INACTIVE) {
- status
- } else {
- val msg = s"""'$status' is not a recognized rule state, must be one of ['${Status.ACTIVE}', '${Status.INACTIVE}']"""
- deserializationError(msg)
- }
- case Failure(t) => deserializationError(t.getMessage)
- }
+ protected[core] def next(status: Status): Status = {
+ status match {
+ case ACTIVE => INACTIVE
+ case INACTIVE => ACTIVE
}
+ }
+
+ /**
+ * Creates a rule Status from a string.
+ *
+ * @param str the rule status as string
+ * @return Status instance
+ * @throws IllegalArgumentException is argument is undefined or not a valid status
+ */
+ @throws[IllegalArgumentException]
+ override protected[entity] def factory(str: String): Status = {
+ val status = new Status(str)
+ require(status == ACTIVE || status == INACTIVE, s"$str is not a recognized rule state")
+ status
+ }
+
+ override protected[core] implicit val serdes = new RootJsonFormat[Status] {
+ def write(s: Status) = JsString(s.status)
+
+ def read(value: JsValue) =
+ Try {
+ val JsString(v) = value
+ Status(v)
+ } match {
+ case Success(s) => s
+ case Failure(t) => deserializationError(t.getMessage)
+ }
+ }
+
+ /**
+ * A serializer for status POST entities. This is a restricted
+ * Status view with only ACTIVE and INACTIVE values allowed.
+ */
+ protected[core] val serdesRestricted = new RootJsonFormat[Status] {
+ def write(s: Status) = JsObject("status" -> JsString(s.status))
+
+ def read(value: JsValue) =
+ Try {
+ val JsObject(fields) = value
+ val JsString(s) = fields("status")
+ Status(s)
+ } match {
+ case Success(status) =>
+ if (status == ACTIVE || status == INACTIVE) {
+ status
+ } else {
+ val msg =
+ s"""'$status' is not a recognized rule state, must be one of ['${Status.ACTIVE}', '${Status.INACTIVE}']"""
+ deserializationError(msg)
+ }
+ case Failure(t) => deserializationError(t.getMessage)
+ }
+ }
}
-object WhiskRule
- extends DocumentFactory[WhiskRule]
- with WhiskEntityQueries[WhiskRule]
- with DefaultJsonProtocol {
-
- override val collectionName = "rules"
-
- private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
- private val caseClassSerdes = jsonFormat7(WhiskRule.apply)
-
- override implicit val serdes = new RootJsonFormat[WhiskRule] {
- def write(r: WhiskRule) = caseClassSerdes.write(r)
-
- def read(value: JsValue) = Try {
- caseClassSerdes.read(value)
- } recover {
- case DeserializationException(_, _, List("trigger")) | DeserializationException(_, _, List("action")) =>
- val namespace = value.asJsObject.fields("namespace").convertTo[EntityPath]
- val actionName = value.asJsObject.fields("action")
- val triggerName = value.asJsObject.fields("trigger")
-
- val refs = Seq(actionName, triggerName).map { name =>
- Try {
- FullyQualifiedEntityName(namespace, EntityName.serdes.read(name))
- } match {
- case Success(n) => n
- case Failure(t) => deserializationError(t.getMessage)
- }
- }
- val fields = value.asJsObject.fields + ("action" -> refs(0).toDocId.toJson) + ("trigger" -> refs(1).toDocId.toJson)
- caseClassSerdes.read(JsObject(fields))
- } match {
- case Success(r) => r
- case Failure(t) => deserializationError(t.getMessage)
- }
- }
-
- override val cacheEnabled = false
+object WhiskRule extends DocumentFactory[WhiskRule] with WhiskEntityQueries[WhiskRule] with DefaultJsonProtocol {
+
+ override val collectionName = "rules"
+
+ private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
+ private val caseClassSerdes = jsonFormat7(WhiskRule.apply)
+
+ override implicit val serdes = new RootJsonFormat[WhiskRule] {
+ def write(r: WhiskRule) = caseClassSerdes.write(r)
+
+ def read(value: JsValue) =
+ Try {
+ caseClassSerdes.read(value)
+ } recover {
+ case DeserializationException(_, _, List("trigger")) | DeserializationException(_, _, List("action")) =>
+ val namespace = value.asJsObject.fields("namespace").convertTo[EntityPath]
+ val actionName = value.asJsObject.fields("action")
+ val triggerName = value.asJsObject.fields("trigger")
+
+ val refs = Seq(actionName, triggerName).map { name =>
+ Try {
+ FullyQualifiedEntityName(namespace, EntityName.serdes.read(name))
+ } match {
+ case Success(n) => n
+ case Failure(t) => deserializationError(t.getMessage)
+ }
+ }
+ val fields = value.asJsObject.fields + ("action" -> refs(0).toDocId.toJson) + ("trigger" -> refs(1).toDocId.toJson)
+ caseClassSerdes.read(JsObject(fields))
+ } match {
+ case Success(r) => r
+ case Failure(t) => deserializationError(t.getMessage)
+ }
+ }
+
+ override val cacheEnabled = false
}
object WhiskRuleResponse extends DefaultJsonProtocol {
- private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
- implicit val serdes = jsonFormat8(WhiskRuleResponse.apply)
+ private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
+ implicit val serdes = jsonFormat8(WhiskRuleResponse.apply)
}
object WhiskRulePut extends DefaultJsonProtocol {
- private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
- implicit val serdes = jsonFormat5(WhiskRulePut.apply)
+ private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
+ implicit val serdes = jsonFormat5(WhiskRulePut.apply)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
index 50a9722..21595b7 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
@@ -47,87 +47,90 @@ import whisk.core.database.StaleParameter
import whisk.spi.SpiLoader
package object types {
- type AuthStore = ArtifactStore[WhiskAuth]
- type EntityStore = ArtifactStore[WhiskEntity]
- type ActivationStore = ArtifactStore[WhiskActivation]
+ type AuthStore = ArtifactStore[WhiskAuth]
+ type EntityStore = ArtifactStore[WhiskEntity]
+ type ActivationStore = ArtifactStore[WhiskActivation]
}
-protected[core] trait WhiskDocument
- extends DocumentSerializer
- with DocumentRevisionProvider {
+protected[core] trait WhiskDocument extends DocumentSerializer with DocumentRevisionProvider {
- /**
- * Gets unique document identifier for the document.
- */
- protected def docid: DocId
+ /**
+ * Gets unique document identifier for the document.
+ */
+ protected def docid: DocId
- /**
- * Creates DocId from the unique document identifier and the
- * document revision if one exists.
- */
- protected[core] final def docinfo: DocInfo = DocInfo(docid, rev)
+ /**
+ * Creates DocId from the unique document identifier and the
+ * document revision if one exists.
+ */
+ protected[core] final def docinfo: DocInfo = DocInfo(docid, rev)
- /**
- * The representation as JSON, e.g. for REST calls. Does not include id/rev.
- */
- def toJson: JsObject
+ /**
+ * The representation as JSON, e.g. for REST calls. Does not include id/rev.
+ */
+ def toJson: JsObject
- /**
- * Database JSON representation. Includes id/rev when appropriate. May
- * differ from `toJson` in exceptional cases.
- */
- override def toDocumentRecord: JsObject = {
- val id = docid.id
- val revOrNull = rev.rev
+ /**
+ * Database JSON representation. Includes id/rev when appropriate. May
+ * differ from `toJson` in exceptional cases.
+ */
+ override def toDocumentRecord: JsObject = {
+ val id = docid.id
+ val revOrNull = rev.rev
- // Building up the fields.
- val base = this.toJson.fields
- val withId = base + ("_id" -> JsString(id))
- val withRev = if (revOrNull == null) withId else { withId + ("_rev" -> JsString(revOrNull)) }
- JsObject(withRev)
- }
+ // Building up the fields.
+ val base = this.toJson.fields
+ val withId = base + ("_id" -> JsString(id))
+ val withRev = if (revOrNull == null) withId else { withId + ("_rev" -> JsString(revOrNull)) }
+ JsObject(withRev)
+ }
}
object WhiskAuthStore {
- def requiredProperties =
- Map(dbProvider -> null,
- dbProtocol -> null,
- dbUsername -> null,
- dbPassword -> null,
- dbHost -> null,
- dbPort -> null,
- dbAuths -> null)
+ def requiredProperties =
+ Map(
+ dbProvider -> null,
+ dbProtocol -> null,
+ dbUsername -> null,
+ dbPassword -> null,
+ dbHost -> null,
+ dbPort -> null,
+ dbAuths -> null)
- def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging: Logging) =
- SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskAuth](config, _.dbAuths)
+ def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging: Logging) =
+ SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskAuth](config, _.dbAuths)
}
object WhiskEntityStore {
- def requiredProperties =
- Map(dbProvider -> null,
- dbProtocol -> null,
- dbUsername -> null,
- dbPassword -> null,
- dbHost -> null,
- dbPort -> null,
- dbWhisk -> null)
+ def requiredProperties =
+ Map(
+ dbProvider -> null,
+ dbProtocol -> null,
+ dbUsername -> null,
+ dbPassword -> null,
+ dbHost -> null,
+ dbPort -> null,
+ dbWhisk -> null)
- def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging: Logging) =
- SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskEntity](config, _.dbWhisk)(WhiskEntityJsonFormat, system, logging)
+ def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging: Logging) =
+ SpiLoader
+ .get[ArtifactStoreProvider]
+ .makeStore[WhiskEntity](config, _.dbWhisk)(WhiskEntityJsonFormat, system, logging)
}
object WhiskActivationStore {
- def requiredProperties =
- Map(dbProvider -> null,
- dbProtocol -> null,
- dbUsername -> null,
- dbPassword -> null,
- dbHost -> null,
- dbPort -> null,
- dbActivations -> null)
+ def requiredProperties =
+ Map(
+ dbProvider -> null,
+ dbProtocol -> null,
+ dbUsername -> null,
+ dbPassword -> null,
+ dbHost -> null,
+ dbPort -> null,
+ dbActivations -> null)
- def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging: Logging) =
- SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskActivation](config, _.dbActivations)
+ def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging: Logging) =
+ SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskActivation](config, _.dbActivations)
}
/**
@@ -163,194 +166,212 @@ object WhiskActivationStore {
* the required views are installed by wipeTransientDBs.sh.
*/
object WhiskEntityQueries {
- val TOP = "\ufff0"
- val WHISKVIEW = "whisks"
- val ALL = "all"
- val ENTITIES = "entities"
+ val TOP = "\ufff0"
+ val WHISKVIEW = "whisks"
+ val ALL = "all"
+ val ENTITIES = "entities"
- /**
- * Determines the view name for the collection. There are two cases: a view
- * that is namespace specific, or namespace agnostic..
- */
- def viewname(collection: String, allNamespaces: Boolean = false): String = {
- if (!allNamespaces) {
- s"$WHISKVIEW/$collection"
- } else s"$WHISKVIEW/$collection-all"
- }
+ /**
+ * Determines the view name for the collection. There are two cases: a view
+ * that is namespace specific, or namespace agnostic..
+ */
+ def viewname(collection: String, allNamespaces: Boolean = false): String = {
+ if (!allNamespaces) {
+ s"$WHISKVIEW/$collection"
+ } else s"$WHISKVIEW/$collection-all"
+ }
- /**
- * Queries the datastore for all entities in a namespace, and converts the list of entities
- * to a map that collects the entities by their type.
- */
- def listAllInNamespace[A <: WhiskEntity](
- db: ArtifactStore[A],
- namespace: EntityPath,
- includeDocs: Boolean,
- stale: StaleParameter = StaleParameter.No)(
- implicit transid: TransactionId): Future[Map[String, List[JsObject]]] = {
- implicit val ec = db.executionContext
- val startKey = List(namespace.toString)
- val endKey = List(namespace.toString, TOP)
- db.query(viewname(ALL), startKey, endKey, 0, 0, includeDocs, descending = true, reduce = false, stale = stale) map {
- _ map {
- row =>
- val value = row.fields("value").asJsObject
- val JsString(collection) = value.fields("collection")
- (collection, JsObject(value.fields.filterNot { _._1 == "collection" }))
- } groupBy { _._1 } mapValues { _.map(_._2) }
- }
+ /**
+ * Queries the datastore for all entities in a namespace, and converts the list of entities
+ * to a map that collects the entities by their type.
+ */
+ def listAllInNamespace[A <: WhiskEntity](
+ db: ArtifactStore[A],
+ namespace: EntityPath,
+ includeDocs: Boolean,
+ stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId): Future[Map[String, List[JsObject]]] = {
+ implicit val ec = db.executionContext
+ val startKey = List(namespace.toString)
+ val endKey = List(namespace.toString, TOP)
+ db.query(viewname(ALL), startKey, endKey, 0, 0, includeDocs, descending = true, reduce = false, stale = stale) map {
+ _ map { row =>
+ val value = row.fields("value").asJsObject
+ val JsString(collection) = value.fields("collection")
+ (collection, JsObject(value.fields.filterNot { _._1 == "collection" }))
+ } groupBy { _._1 } mapValues { _.map(_._2) }
}
+ }
- /**
- * Queries the datastore for all entities without activations in a namespace, and converts the list of entities
- * to a map that collects the entities by their type.
- */
- def listEntitiesInNamespace[A <: WhiskEntity](
- db: ArtifactStore[A],
- namespace: EntityPath,
- includeDocs: Boolean,
- stale: StaleParameter = StaleParameter.No)(
- implicit transid: TransactionId): Future[Map[String, List[JsObject]]] = {
- implicit val ec = db.executionContext
- val startKey = List(namespace.toString)
- val endKey = List(namespace.toString, TOP)
- db.query(viewname(ENTITIES), startKey, endKey, 0, 0, includeDocs, descending = true, reduce = false, stale = stale) map {
- _ map {
- row =>
- val value = row.fields("value").asJsObject
- val JsString(collection) = value.fields("collection")
- (collection, JsObject(value.fields.filterNot { _._1 == "collection" }))
- } groupBy { _._1 } mapValues { _.map(_._2) }
- }
+ /**
+ * Queries the datastore for all entities without activations in a namespace, and converts the list of entities
+ * to a map that collects the entities by their type.
+ */
+ def listEntitiesInNamespace[A <: WhiskEntity](
+ db: ArtifactStore[A],
+ namespace: EntityPath,
+ includeDocs: Boolean,
+ stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId): Future[Map[String, List[JsObject]]] = {
+ implicit val ec = db.executionContext
+ val startKey = List(namespace.toString)
+ val endKey = List(namespace.toString, TOP)
+ db.query(viewname(ENTITIES), startKey, endKey, 0, 0, includeDocs, descending = true, reduce = false, stale = stale) map {
+ _ map { row =>
+ val value = row.fields("value").asJsObject
+ val JsString(collection) = value.fields("collection")
+ (collection, JsObject(value.fields.filterNot { _._1 == "collection" }))
+ } groupBy { _._1 } mapValues { _.map(_._2) }
}
+ }
- def listCollectionInAnyNamespace[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- collection: String,
- skip: Int,
- limit: Int,
- reduce: Boolean,
- since: Option[Instant] = None,
- upto: Option[Instant] = None,
- stale: StaleParameter = StaleParameter.No,
- convert: Option[JsObject => Try[T]])(
- implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
- val startKey = List(since map { _.toEpochMilli } getOrElse 0)
- val endKey = List(upto map { _.toEpochMilli } getOrElse TOP, TOP)
- query(db, viewname(collection, true), startKey, endKey, skip, limit, reduce, stale, convert)
- }
+ def listCollectionInAnyNamespace[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ collection: String,
+ skip: Int,
+ limit: Int,
+ reduce: Boolean,
+ since: Option[Instant] = None,
+ upto: Option[Instant] = None,
+ stale: StaleParameter = StaleParameter.No,
+ convert: Option[JsObject => Try[T]])(implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
+ val startKey = List(since map { _.toEpochMilli } getOrElse 0)
+ val endKey = List(upto map { _.toEpochMilli } getOrElse TOP, TOP)
+ query(db, viewname(collection, true), startKey, endKey, skip, limit, reduce, stale, convert)
+ }
- def listCollectionInNamespace[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- collection: String,
- namespace: EntityPath,
- skip: Int,
- limit: Int,
- since: Option[Instant] = None,
- upto: Option[Instant] = None,
- stale: StaleParameter = StaleParameter.No,
- convert: Option[JsObject => Try[T]])(
- implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
- val startKey = List(namespace.toString, since map { _.toEpochMilli } getOrElse 0)
- val endKey = List(namespace.toString, upto map { _.toEpochMilli } getOrElse TOP, TOP)
- query(db, viewname(collection), startKey, endKey, skip, limit, reduce = false, stale, convert)
- }
+ def listCollectionInNamespace[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ collection: String,
+ namespace: EntityPath,
+ skip: Int,
+ limit: Int,
+ since: Option[Instant] = None,
+ upto: Option[Instant] = None,
+ stale: StaleParameter = StaleParameter.No,
+ convert: Option[JsObject => Try[T]])(implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
+ val startKey = List(namespace.toString, since map { _.toEpochMilli } getOrElse 0)
+ val endKey = List(namespace.toString, upto map { _.toEpochMilli } getOrElse TOP, TOP)
+ query(db, viewname(collection), startKey, endKey, skip, limit, reduce = false, stale, convert)
+ }
- def listCollectionByName[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- collection: String,
- namespace: EntityPath,
- name: EntityName,
- skip: Int,
- limit: Int,
- since: Option[Instant] = None,
- upto: Option[Instant] = None,
- stale: StaleParameter = StaleParameter.No,
- convert: Option[JsObject => Try[T]])(
- implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
- val startKey = List(namespace.addPath(name).toString, since map { _.toEpochMilli } getOrElse 0)
- val endKey = List(namespace.addPath(name).toString, upto map { _.toEpochMilli } getOrElse TOP, TOP)
- query(db, viewname(collection), startKey, endKey, skip, limit, reduce = false, stale, convert)
- }
+ def listCollectionByName[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ collection: String,
+ namespace: EntityPath,
+ name: EntityName,
+ skip: Int,
+ limit: Int,
+ since: Option[Instant] = None,
+ upto: Option[Instant] = None,
+ stale: StaleParameter = StaleParameter.No,
+ convert: Option[JsObject => Try[T]])(implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
+ val startKey = List(namespace.addPath(name).toString, since map { _.toEpochMilli } getOrElse 0)
+ val endKey = List(namespace.addPath(name).toString, upto map { _.toEpochMilli } getOrElse TOP, TOP)
+ query(db, viewname(collection), startKey, endKey, skip, limit, reduce = false, stale, convert)
+ }
- private def query[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- view: String,
- startKey: List[Any],
- endKey: List[Any],
- skip: Int,
- limit: Int,
- reduce: Boolean,
- stale: StaleParameter = StaleParameter.No,
- convert: Option[JsObject => Try[T]])(
- implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
- implicit val ec = db.executionContext
- val includeDocs = convert.isDefined
- db.query(view, startKey, endKey, skip, limit, includeDocs, true, reduce, stale) map {
- rows =>
- convert map { fn =>
- Right(rows flatMap { row => fn(row.fields("doc").asJsObject) toOption })
- } getOrElse {
- Left(rows flatMap { normalizeRow(_, reduce) toOption })
- }
- }
+ private def query[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ view: String,
+ startKey: List[Any],
+ endKey: List[Any],
+ skip: Int,
+ limit: Int,
+ reduce: Boolean,
+ stale: StaleParameter = StaleParameter.No,
+ convert: Option[JsObject => Try[T]])(implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {
+ implicit val ec = db.executionContext
+ val includeDocs = convert.isDefined
+ db.query(view, startKey, endKey, skip, limit, includeDocs, true, reduce, stale) map { rows =>
+ convert map { fn =>
+ Right(rows flatMap { row =>
+ fn(row.fields("doc").asJsObject) toOption
+ })
+ } getOrElse {
+ Left(rows flatMap { normalizeRow(_, reduce) toOption })
+ }
}
+ }
- /**
- * Normalizes the raw JsObject response from the datastore since the
- * response differs in the case of a reduction.
- */
- protected def normalizeRow(row: JsObject, reduce: Boolean) = Try {
- if (!reduce) {
- row.fields("value").asJsObject
- } else row
- }
+ /**
+ * Normalizes the raw JsObject response from the datastore since the
+ * response differs in the case of a reduction.
+ */
+ protected def normalizeRow(row: JsObject, reduce: Boolean) = Try {
+ if (!reduce) {
+ row.fields("value").asJsObject
+ } else row
+ }
}
trait WhiskEntityQueries[T] {
- val collectionName: String
- val serdes: RootJsonFormat[T]
+ val collectionName: String
+ val serdes: RootJsonFormat[T]
- def listCollectionInAnyNamespace[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- skip: Int,
- limit: Int,
- docs: Boolean = false,
- reduce: Boolean = false,
- since: Option[Instant] = None,
- upto: Option[Instant] = None,
- stale: StaleParameter = StaleParameter.No)(
- implicit transid: TransactionId) = {
- val convert = if (docs) Some((o: JsObject) => Try { serdes.read(o) }) else None
- WhiskEntityQueries.listCollectionInAnyNamespace(db, collectionName, skip, limit, reduce, since, upto, stale, convert)
- }
+ def listCollectionInAnyNamespace[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ skip: Int,
+ limit: Int,
+ docs: Boolean = false,
+ reduce: Boolean = false,
+ since: Option[Instant] = None,
+ upto: Option[Instant] = None,
+ stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId) = {
+ val convert = if (docs) Some((o: JsObject) => Try { serdes.read(o) }) else None
+ WhiskEntityQueries.listCollectionInAnyNamespace(
+ db,
+ collectionName,
+ skip,
+ limit,
+ reduce,
+ since,
+ upto,
+ stale,
+ convert)
+ }
- def listCollectionInNamespace[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- namespace: EntityPath,
- skip: Int,
- limit: Int,
- docs: Boolean = false,
- since: Option[Instant] = None,
- upto: Option[Instant] = None,
- stale: StaleParameter = StaleParameter.No)(
- implicit transid: TransactionId) = {
- val convert = if (docs) Some((o: JsObject) => Try { serdes.read(o) }) else None
- WhiskEntityQueries.listCollectionInNamespace(db, collectionName, namespace, skip, limit, since, upto, stale, convert)
- }
+ def listCollectionInNamespace[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ namespace: EntityPath,
+ skip: Int,
+ limit: Int,
+ docs: Boolean = false,
+ since: Option[Instant] = None,
+ upto: Option[Instant] = None,
+ stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId) = {
+ val convert = if (docs) Some((o: JsObject) => Try { serdes.read(o) }) else None
+ WhiskEntityQueries.listCollectionInNamespace(
+ db,
+ collectionName,
+ namespace,
+ skip,
+ limit,
+ since,
+ upto,
+ stale,
+ convert)
+ }
- def listCollectionByName[A <: WhiskEntity, T](
- db: ArtifactStore[A],
- namespace: EntityPath,
- name: EntityName,
- skip: Int,
- limit: Int,
- docs: Boolean = false,
- since: Option[Instant] = None,
- upto: Option[Instant] = None,
- stale: StaleParameter = StaleParameter.No)(
- implicit transid: TransactionId) = {
- val convert = if (docs) Some((o: JsObject) => Try { serdes.read(o) }) else None
- WhiskEntityQueries.listCollectionByName(db, collectionName, namespace, name, skip, limit, since, upto, stale, convert)
- }
+ def listCollectionByName[A <: WhiskEntity, T](
+ db: ArtifactStore[A],
+ namespace: EntityPath,
+ name: EntityName,
+ skip: Int,
+ limit: Int,
+ docs: Boolean = false,
+ since: Option[Instant] = None,
+ upto: Option[Instant] = None,
+ stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId) = {
+ val convert = if (docs) Some((o: JsObject) => Try { serdes.read(o) }) else None
+ WhiskEntityQueries.listCollectionByName(
+ db,
+ collectionName,
+ namespace,
+ name,
+ skip,
+ limit,
+ since,
+ upto,
+ stale,
+ convert)
+ }
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskTrigger.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskTrigger.scala
index d2271c4..ec10579 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskTrigger.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskTrigger.scala
@@ -25,12 +25,11 @@ import spray.json._
* WhiskTriggerPut is a restricted WhiskTrigger view that eschews properties
* that are auto-assigned or derived from URI: namespace and name.
*/
-case class WhiskTriggerPut(
- parameters: Option[Parameters] = None,
- limits: Option[TriggerLimits] = None,
- version: Option[SemVer] = None,
- publish: Option[Boolean] = None,
- annotations: Option[Parameters] = None)
+case class WhiskTriggerPut(parameters: Option[Parameters] = None,
+ limits: Option[TriggerLimits] = None,
+ version: Option[SemVer] = None,
+ publish: Option[Boolean] = None,
+ annotations: Option[Parameters] = None)
/**
* Representation of a rule to be stored inside a trigger. Contains all
@@ -40,9 +39,7 @@ case class WhiskTriggerPut(
* @param action the fully qualified name of the action to be fired
* @param status status of the rule
*/
-case class ReducedRule(
- action: FullyQualifiedEntityName,
- status: Status)
+case class ReducedRule(action: FullyQualifiedEntityName, status: Status)
/**
* A WhiskTrigger provides an abstraction of the meta-data
@@ -62,50 +59,49 @@ case class ReducedRule(
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
-case class WhiskTrigger(
- namespace: EntityPath,
- override val name: EntityName,
- parameters: Parameters = Parameters(),
- limits: TriggerLimits = TriggerLimits(),
- version: SemVer = SemVer(),
- publish: Boolean = false,
- annotations: Parameters = Parameters(),
- rules: Option[Map[FullyQualifiedEntityName, ReducedRule]] = None)
+case class WhiskTrigger(namespace: EntityPath,
+ override val name: EntityName,
+ parameters: Parameters = Parameters(),
+ limits: TriggerLimits = TriggerLimits(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters(),
+ rules: Option[Map[FullyQualifiedEntityName, ReducedRule]] = None)
extends WhiskEntity(name) {
- require(limits != null, "limits undefined")
-
- def toJson = WhiskTrigger.serdes.write(this).asJsObject
-
- def withoutRules = copy(rules = None).revision[WhiskTrigger](rev)
-
- /**
- * Inserts the rulename, its status and the action to be fired into the trigger.
- *
- * @param rulename The fully qualified name of the rule, that will be fired by this trigger.
- * @param rule The rule, that will be fired by this trigger. It's from type ReducedRule. This type
- * contains the fully qualified name of the action to be fired by the rule and the status of the rule.
- */
- def addRule(rulename: FullyQualifiedEntityName, rule: ReducedRule) = {
- val entry = rulename -> rule
- val links = rules getOrElse Map.empty[FullyQualifiedEntityName, ReducedRule]
- copy(rules = Some(links + entry)).revision[WhiskTrigger](docinfo.rev)
- }
-
- /**
- * Removes the rule from the trigger.
- *
- * @param rule The fully qualified name of the rule, that should be removed from the
- * trigger. After removing the rule, it won't be fired anymore by this trigger.
- */
- def removeRule(rule: FullyQualifiedEntityName) = {
- copy(rules = rules.map(_ - rule)).revision[WhiskTrigger](docinfo.rev)
- }
+ require(limits != null, "limits undefined")
+
+ def toJson = WhiskTrigger.serdes.write(this).asJsObject
+
+ def withoutRules = copy(rules = None).revision[WhiskTrigger](rev)
+
+ /**
+ * Inserts the rulename, its status and the action to be fired into the trigger.
+ *
+ * @param rulename The fully qualified name of the rule, that will be fired by this trigger.
+ * @param rule The rule, that will be fired by this trigger. It's from type ReducedRule. This type
+ * contains the fully qualified name of the action to be fired by the rule and the status of the rule.
+ */
+ def addRule(rulename: FullyQualifiedEntityName, rule: ReducedRule) = {
+ val entry = rulename -> rule
+ val links = rules getOrElse Map.empty[FullyQualifiedEntityName, ReducedRule]
+ copy(rules = Some(links + entry)).revision[WhiskTrigger](docinfo.rev)
+ }
+
+ /**
+ * Removes the rule from the trigger.
+ *
+ * @param rule The fully qualified name of the rule, that should be removed from the
+ * trigger. After removing the rule, it won't be fired anymore by this trigger.
+ */
+ def removeRule(rule: FullyQualifiedEntityName) = {
+ copy(rules = rules.map(_ - rule)).revision[WhiskTrigger](docinfo.rev)
+ }
}
object ReducedRule extends DefaultJsonProtocol {
- private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
- implicit val serdes = jsonFormat2(ReducedRule.apply)
+ private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
+ implicit val serdes = jsonFormat2(ReducedRule.apply)
}
object WhiskTrigger
@@ -113,14 +109,14 @@ object WhiskTrigger
with WhiskEntityQueries[WhiskTrigger]
with DefaultJsonProtocol {
- override val collectionName = "triggers"
+ override val collectionName = "triggers"
- private implicit val fqnSerdesAsDocId = FullyQualifiedEntityName.serdesAsDocId
- override implicit val serdes = jsonFormat8(WhiskTrigger.apply)
+ private implicit val fqnSerdesAsDocId = FullyQualifiedEntityName.serdesAsDocId
+ override implicit val serdes = jsonFormat8(WhiskTrigger.apply)
- override val cacheEnabled = true
+ override val cacheEnabled = true
}
object WhiskTriggerPut extends DefaultJsonProtocol {
- implicit val serdes = jsonFormat5(WhiskTriggerPut.apply)
+ implicit val serdes = jsonFormat5(WhiskTriggerPut.apply)
}
diff --git a/common/scala/src/main/scala/whisk/http/BasicHttpService.scala b/common/scala/src/main/scala/whisk/http/BasicHttpService.scala
index 502d0e9..f250ddf 100644
--- a/common/scala/src/main/scala/whisk/http/BasicHttpService.scala
+++ b/common/scala/src/main/scala/whisk/http/BasicHttpService.scala
@@ -43,100 +43,102 @@ import whisk.common.TransactionId
*/
trait BasicHttpService extends Directives with TransactionCounter {
- /** Rejection handler to terminate connection on a bad request. Delegates to Akka handler. */
- implicit def customRejectionHandler(implicit transid: TransactionId) = {
- RejectionHandler.default.mapRejectionResponse {
- case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) =>
- val error = ErrorResponse(ent.data.utf8String, transid).toJson
- res.copy(entity = HttpEntity(ContentTypes.`application/json`, error.compactPrint))
- case x => x
- }
+ /** Rejection handler to terminate connection on a bad request. Delegates to Akka handler. */
+ implicit def customRejectionHandler(implicit transid: TransactionId) = {
+ RejectionHandler.default.mapRejectionResponse {
+ case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) =>
+ val error = ErrorResponse(ent.data.utf8String, transid).toJson
+ res.copy(entity = HttpEntity(ContentTypes.`application/json`, error.compactPrint))
+ case x => x
}
-
- /**
- * Gets the routes implemented by the HTTP service.
- *
- * @param transid the id for the transaction (every request is assigned an id)
- */
- def routes(implicit transid: TransactionId): Route
-
- /**
- * Gets the log level for a given route. The default is
- * InfoLevel so override as needed.
- *
- * @param route the route to determine the loglevel for
- * @return a log level for the route
- */
- def loglevelForRoute(route: String): Logging.LogLevel = Logging.InfoLevel
-
- /** Rejection handler to terminate connection on a bad request. Delegates to Akka handler. */
- val prioritizeRejections = recoverRejections { rejections =>
- val priorityRejection = rejections.find {
- case rejection: UnacceptedResponseContentTypeRejection => true
- case _ => false
- }
-
- priorityRejection.map(rejection => Rejected(Seq(rejection))).getOrElse(Rejected(rejections))
+ }
+
+ /**
+ * Gets the routes implemented by the HTTP service.
+ *
+ * @param transid the id for the transaction (every request is assigned an id)
+ */
+ def routes(implicit transid: TransactionId): Route
+
+ /**
+ * Gets the log level for a given route. The default is
+ * InfoLevel so override as needed.
+ *
+ * @param route the route to determine the loglevel for
+ * @return a log level for the route
+ */
+ def loglevelForRoute(route: String): Logging.LogLevel = Logging.InfoLevel
+
+ /** Rejection handler to terminate connection on a bad request. Delegates to Akka handler. */
+ val prioritizeRejections = recoverRejections { rejections =>
+ val priorityRejection = rejections.find {
+ case rejection: UnacceptedResponseContentTypeRejection => true
+ case _ => false
}
- /**
- * Receives a message and runs the router.
- */
- def route: Route = {
- assignId { implicit transid =>
- DebuggingDirectives.logRequest(logRequestInfo _) {
- DebuggingDirectives.logRequestResult(logResponseInfo _) {
- handleRejections(customRejectionHandler) {
- prioritizeRejections {
- toStrictEntity(30.seconds) {
- routes
- }
- }
- }
- }
+ priorityRejection.map(rejection => Rejected(Seq(rejection))).getOrElse(Rejected(rejections))
+ }
+
+ /**
+ * Receives a message and runs the router.
+ */
+ def route: Route = {
+ assignId { implicit transid =>
+ DebuggingDirectives.logRequest(logRequestInfo _) {
+ DebuggingDirectives.logRequestResult(logResponseInfo _) {
+ handleRejections(customRejectionHandler) {
+ prioritizeRejections {
+ toStrictEntity(30.seconds) {
+ routes
+ }
}
+ }
}
+ }
}
-
- /** Assigns transaction id to every request. */
- protected val assignId = extract(_ => transid())
-
- /** Generates log entry for every request. */
- protected def logRequestInfo(req: HttpRequest)(implicit tid: TransactionId): LogEntry = {
- val m = req.method.name
- val p = req.uri.path.toString
- val q = req.uri.query().toString
- val l = loglevelForRoute(p)
- LogEntry(s"[$tid] $m $p $q", l)
- }
-
- protected def logResponseInfo(req: HttpRequest)(implicit tid: TransactionId): RouteResult => Option[LogEntry] = {
- case RouteResult.Complete(res: HttpResponse) =>
- val m = req.method.name
- val p = req.uri.path.toString
- val l = loglevelForRoute(p)
-
- val name = "BasicHttpService"
-
- val token = LogMarkerToken("http", s"${m.toLowerCase}.${res.status.intValue}", LoggingMarkers.count)
- val marker = LogMarker(token, tid.deltaToStart, Some(tid.deltaToStart))
-
- Some(LogEntry(s"[$tid] [$name] $marker", l))
- case _ => None // other kind of responses
- }
+ }
+
+ /** Assigns transaction id to every request. */
+ protected val assignId = extract(_ => transid())
+
+ /** Generates log entry for every request. */
+ protected def logRequestInfo(req: HttpRequest)(implicit tid: TransactionId): LogEntry = {
+ val m = req.method.name
+ val p = req.uri.path.toString
+ val q = req.uri.query().toString
+ val l = loglevelForRoute(p)
+ LogEntry(s"[$tid] $m $p $q", l)
+ }
+
+ protected def logResponseInfo(req: HttpRequest)(implicit tid: TransactionId): RouteResult => Option[LogEntry] = {
+ case RouteResult.Complete(res: HttpResponse) =>
+ val m = req.method.name
+ val p = req.uri.path.toString
+ val l = loglevelForRoute(p)
+
+ val name = "BasicHttpService"
+
+ val token = LogMarkerToken("http", s"${m.toLowerCase}.${res.status.intValue}", LoggingMarkers.count)
+ val marker = LogMarker(token, tid.deltaToStart, Some(tid.deltaToStart))
+
+ Some(LogEntry(s"[$tid] [$name] $marker", l))
+ case _ => None // other kind of responses
+ }
}
object BasicHttpService {
- /**
- * Starts an HTTP route handler on given port and registers a shutdown hook.
- */
- def startService(route: Route, port: Int)(implicit actorSystem: ActorSystem, materializer: ActorMaterializer): Unit = {
- implicit val executionContext = actorSystem.dispatcher
- val httpBinding = Http().bindAndHandle(route, "0.0.0.0", port)
- sys.addShutdownHook {
- Await.result(httpBinding.map(_.unbind()), 30.seconds)
- actorSystem.terminate()
- Await.result(actorSystem.whenTerminated, 30.seconds)
- }
+
+ /**
+ * Starts an HTTP route handler on given port and registers a shutdown hook.
+ */
+ def startService(route: Route, port: Int)(implicit actorSystem: ActorSystem,
+ materializer: ActorMaterializer): Unit = {
+ implicit val executionContext = actorSystem.dispatcher
+ val httpBinding = Http().bindAndHandle(route, "0.0.0.0", port)
+ sys.addShutdownHook {
+ Await.result(httpBinding.map(_.unbind()), 30.seconds)
+ actorSystem.terminate()
+ Await.result(actorSystem.whenTerminated, 30.seconds)
}
+ }
}
diff --git a/common/scala/src/main/scala/whisk/http/BasicRasService.scala b/common/scala/src/main/scala/whisk/http/BasicRasService.scala
index 4c773d0..cb1e017 100644
--- a/common/scala/src/main/scala/whisk/http/BasicRasService.scala
+++ b/common/scala/src/main/scala/whisk/http/BasicRasService.scala
@@ -28,17 +28,17 @@ import whisk.common.TransactionId
*/
trait BasicRasService extends BasicHttpService {
- override def routes(implicit transid: TransactionId) = ping
+ override def routes(implicit transid: TransactionId) = ping
- override def loglevelForRoute(route: String): Logging.LogLevel = {
- if (route == "/ping") {
- Logging.DebugLevel
- } else {
- super.loglevelForRoute(route)
- }
+ override def loglevelForRoute(route: String): Logging.LogLevel = {
+ if (route == "/ping") {
+ Logging.DebugLevel
+ } else {
+ super.loglevelForRoute(route)
}
+ }
- val ping = path("ping") {
- get { complete("pong") }
- }
+ val ping = path("ping") {
+ get { complete("pong") }
+ }
}
diff --git a/common/scala/src/main/scala/whisk/http/ErrorResponse.scala b/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
index 85f39b6..98db5e8 100644
--- a/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
+++ b/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
@@ -38,130 +38,138 @@ import whisk.core.entity.Exec
import whisk.core.entity.ActivationId
object Messages {
- /** Standard message for reporting resource conflicts. */
- val conflictMessage = "Concurrent modification to resource detected."
- /**
- * Standard message for reporting resource conformance error when trying to access
- * a resource from a different collection.
- */
- val conformanceMessage = "Resource by this name exists but is not in this collection."
- val corruptedEntity = "Resource is corrupted and cannot be read."
-
- /**
- * Standard message for reporting deprecated runtimes.
- */
- def runtimeDeprecated(e: Exec) = s"The '${e.kind}' runtime is no longer supported. You may read and delete but not update or invoke this action."
-
- /** Standard message for resource not found. */
- val resourceDoesNotExist = "The requested resource does not exist."
-
- /** Standard message for too many activation requests within a rolling time window. */
- val tooManyRequests = "Too many requests in a given amount of time for namespace."
-
- /** Standard message for too many concurrent activation requests within a time window. */
- val tooManyConcurrentRequests = "Too many concurrent requests in flight for namespace."
-
- /** System overload message. */
- val systemOverloaded = "System is overloaded, try again later."
-
- /** Standard message when supplied authkey is not authorized for an operation. */
- val notAuthorizedtoOperateOnResource = "The supplied authentication is not authorized to access this resource."
-
- /** Standard error message for malformed fully qualified entity names. */
- val malformedFullyQualifiedEntityName = "The fully qualified name of the entity must contain at least the namespace and the name of the entity."
- def entityNameTooLong(error: SizeError) = {
- s"${error.field} longer than allowed: ${error.is.toBytes} > ${error.allowed.toBytes}."
+ /** Standard message for reporting resource conflicts. */
+ val conflictMessage = "Concurrent modification to resource detected."
+
+ /**
+ * Standard message for reporting resource conformance error when trying to access
+ * a resource from a different collection.
+ */
+ val conformanceMessage = "Resource by this name exists but is not in this collection."
+ val corruptedEntity = "Resource is corrupted and cannot be read."
+
+ /**
+ * Standard message for reporting deprecated runtimes.
+ */
+ def runtimeDeprecated(e: Exec) =
+ s"The '${e.kind}' runtime is no longer supported. You may read and delete but not update or invoke this action."
+
+ /** Standard message for resource not found. */
+ val resourceDoesNotExist = "The requested resource does not exist."
+
+ /** Standard message for too many activation requests within a rolling time window. */
+ val tooManyRequests = "Too many requests in a given amount of time for namespace."
+
+ /** Standard message for too many concurrent activation requests within a time window. */
+ val tooManyConcurrentRequests = "Too many concurrent requests in flight for namespace."
+
+ /** System overload message. */
+ val systemOverloaded = "System is overloaded, try again later."
+
+ /** Standard message when supplied authkey is not authorized for an operation. */
+ val notAuthorizedtoOperateOnResource = "The supplied authentication is not authorized to access this resource."
+
+ /** Standard error message for malformed fully qualified entity names. */
+ val malformedFullyQualifiedEntityName =
+ "The fully qualified name of the entity must contain at least the namespace and the name of the entity."
+ def entityNameTooLong(error: SizeError) = {
+ s"${error.field} longer than allowed: ${error.is.toBytes} > ${error.allowed.toBytes}."
+ }
+ val entityNameIllegal = "The name of the entity contains illegal characters."
+ val namespaceIllegal = "The namespace contains illegal characters."
+
+ /** Standard error for malformed activation id. */
+ val activationIdIllegal = "The activation id is not valid."
+ def activationIdLengthError(error: SizeError) = {
+ s"${error.field} length is ${error.is.toBytes} but must be ${error.allowed.toBytes}."
+ }
+
+ /** Error messages for sequence actions. */
+ val sequenceIsTooLong = "Too many actions in the sequence."
+ val sequenceNoComponent = "No component specified for the sequence."
+ val sequenceIsCyclic = "Sequence may not refer to itself."
+ val sequenceComponentNotFound = "Sequence component does not exist."
+
+ /** Error message for packages. */
+ val bindingDoesNotExist = "Binding references a package that does not exist."
+ val packageCannotBecomeBinding = "Resource is a package and cannot be converted into a binding."
+ val bindingCannotReferenceBinding = "Cannot bind to another package binding."
+ val requestedBindingIsNotValid = "Cannot bind to a resource that is not a package."
+ val notAllowedOnBinding = "Operation not permitted on package binding."
+
+ /** Error messages for sequence activations. */
+ def sequenceRetrieveActivationTimeout(id: ActivationId) =
+ s"Timeout reached when retrieving activation $id for sequence component."
+ val sequenceActivationFailure = "Sequence failed."
+
+ /** Error messages for bad requests where parameters do not conform. */
+ val parametersNotAllowed = "Request defines parameters that are not allowed (e.g., reserved properties)."
+ def invalidTimeout(max: FiniteDuration) = s"Timeout must be number of milliseconds up to ${max.toMillis}."
+
+ /** Error messages for activations. */
+ val abnormalInitialization = "The action did not initialize and exited unexpectedly."
+ val abnormalRun = "The action did not produce a valid response and exited unexpectedly."
+ def badEntityName(value: String) = s"Parameter is not a valid value for a entity name: $value"
+ def badNamespace(value: String) = s"Parameter is not a valid value for a namespace: $value"
+ def badEpoch(value: String) = s"Parameter is not a valid value for epoch seconds: $value"
+
+ /** Error message for size conformance. */
+ def entityTooBig(error: SizeError) = {
+ s"${error.field} larger than allowed: ${error.is.toBytes} > ${error.allowed.toBytes} bytes."
+ }
+ def maxActivationLimitExceeded(value: Int, max: Int) = s"Activation limit of $value exceeds maximum limit of $max."
+
+ def truncateLogs(limit: ByteSize) = {
+ s"Logs were truncated because the total bytes size exceeds the limit of ${limit.toBytes} bytes."
+ }
+
+ /** Error for meta api. */
+ val propertyNotFound = "Response does not include requested property."
+ def invalidMedia(m: MediaType) = s"Response is not valid '${m.value}'."
+ def contentTypeExtensionNotSupported(extensions: Set[String]) = {
+ s"""Extension must be specified and one of ${extensions.mkString("[", ", ", "]")}."""
+ }
+ val unsupportedContentType = """Content type is not supported."""
+ def unsupportedContentType(m: MediaType) = s"""Content type '${m.value}' is not supported."""
+ val errorExtractingRequestBody = "Failed extracting request body."
+
+ val responseNotReady = "Response not yet ready."
+ val httpUnknownContentType = "Response did not specify a known content-type."
+ val httpContentTypeError = "Response type in header did not match generated content type."
+ val errorProcessingRequest = "There was an error processing your request."
+
+ def invalidInitResponse(actualResponse: String) = {
+ "The action failed during initialization" + {
+ Option(actualResponse) filter { _.nonEmpty } map { s =>
+ s": $s"
+ } getOrElse "."
}
- val entityNameIllegal = "The name of the entity contains illegal characters."
- val namespaceIllegal = "The namespace contains illegal characters."
+ }
- /** Standard error for malformed activation id. */
- val activationIdIllegal = "The activation id is not valid."
- def activationIdLengthError(error: SizeError) = {
- s"${error.field} length is ${error.is.toBytes} but must be ${error.allowed.toBytes}."
+ def invalidRunResponse(actualResponse: String) = {
+ "The action did not produce a valid JSON response" + {
+ Option(actualResponse) filter { _.nonEmpty } map { s =>
+ s": $s"
+ } getOrElse "."
}
+ }
- /** Error messages for sequence actions. */
- val sequenceIsTooLong = "Too many actions in the sequence."
- val sequenceNoComponent = "No component specified for the sequence."
- val sequenceIsCyclic = "Sequence may not refer to itself."
- val sequenceComponentNotFound = "Sequence component does not exist."
-
- /** Error message for packages. */
- val bindingDoesNotExist = "Binding references a package that does not exist."
- val packageCannotBecomeBinding = "Resource is a package and cannot be converted into a binding."
- val bindingCannotReferenceBinding = "Cannot bind to another package binding."
- val requestedBindingIsNotValid = "Cannot bind to a resource that is not a package."
- val notAllowedOnBinding = "Operation not permitted on package binding."
-
- /** Error messages for sequence activations. */
- def sequenceRetrieveActivationTimeout(id: ActivationId) = s"Timeout reached when retrieving activation $id for sequence component."
- val sequenceActivationFailure = "Sequence failed."
-
- /** Error messages for bad requests where parameters do not conform. */
- val parametersNotAllowed = "Request defines parameters that are not allowed (e.g., reserved properties)."
- def invalidTimeout(max: FiniteDuration) = s"Timeout must be number of milliseconds up to ${max.toMillis}."
-
- /** Error messages for activations. */
- val abnormalInitialization = "The action did not initialize and exited unexpectedly."
- val abnormalRun = "The action did not produce a valid response and exited unexpectedly."
- def badEntityName(value: String) = s"Parameter is not a valid value for a entity name: $value"
- def badNamespace(value: String) = s"Parameter is not a valid value for a namespace: $value"
- def badEpoch(value: String) = s"Parameter is not a valid value for epoch seconds: $value"
-
- /** Error message for size conformance. */
- def entityTooBig(error: SizeError) = {
- s"${error.field} larger than allowed: ${error.is.toBytes} > ${error.allowed.toBytes} bytes."
- }
- def maxActivationLimitExceeded(value: Int, max: Int) = s"Activation limit of $value exceeds maximum limit of $max."
+ def truncatedResponse(length: ByteSize, maxLength: ByteSize): String = {
+ s"The action produced a response that exceeded the allowed length: ${length.toBytes} > ${maxLength.toBytes} bytes."
+ }
- def truncateLogs(limit: ByteSize) = {
- s"Logs were truncated because the total bytes size exceeds the limit of ${limit.toBytes} bytes."
- }
+ def truncatedResponse(trunk: String, length: ByteSize, maxLength: ByteSize): String = {
+ s"${truncatedResponse(length, maxLength)} The truncated response was: $trunk"
+ }
- /** Error for meta api. */
- val propertyNotFound = "Response does not include requested property."
- def invalidMedia(m: MediaType) = s"Response is not valid '${m.value}'."
- def contentTypeExtensionNotSupported(extensions: Set[String]) = {
- s"""Extension must be specified and one of ${extensions.mkString("[", ", ", "]")}."""
- }
- val unsupportedContentType = """Content type is not supported."""
- def unsupportedContentType(m: MediaType) = s"""Content type '${m.value}' is not supported."""
- val errorExtractingRequestBody = "Failed extracting request body."
-
- val responseNotReady = "Response not yet ready."
- val httpUnknownContentType = "Response did not specify a known content-type."
- val httpContentTypeError = "Response type in header did not match generated content type."
- val errorProcessingRequest = "There was an error processing your request."
-
- def invalidInitResponse(actualResponse: String) = {
- "The action failed during initialization" + {
- Option(actualResponse) filter { _.nonEmpty } map { s => s": $s" } getOrElse "."
- }
- }
-
- def invalidRunResponse(actualResponse: String) = {
- "The action did not produce a valid JSON response" + {
- Option(actualResponse) filter { _.nonEmpty } map { s => s": $s" } getOrElse "."
- }
+ def timedoutActivation(timeout: Duration, init: Boolean) = {
+ s"The action exceeded its time limits of ${timeout.toMillis} milliseconds" + {
+ if (!init) "." else " during initialization."
}
+ }
- def truncatedResponse(length: ByteSize, maxLength: ByteSize): String = {
- s"The action produced a response that exceeded the allowed length: ${length.toBytes} > ${maxLength.toBytes} bytes."
- }
-
- def truncatedResponse(trunk: String, length: ByteSize, maxLength: ByteSize): String = {
- s"${truncatedResponse(length, maxLength)} The truncated response was: $trunk"
- }
-
- def timedoutActivation(timeout: Duration, init: Boolean) = {
- s"The action exceeded its time limits of ${timeout.toMillis} milliseconds" + {
- if (!init) "." else " during initialization."
- }
- }
-
- val actionRemovedWhileInvoking = "Action could not be found or may have been deleted."
+ val actionRemovedWhileInvoking = "Action could not be found or may have been deleted."
}
/** Replaces rejections with Json object containing cause and transaction id. */
@@ -169,42 +177,42 @@ case class ErrorResponse(error: String, code: TransactionId)
object ErrorResponse extends Directives with DefaultJsonProtocol {
- def terminate(status: StatusCode, error: String)(
- implicit transid: TransactionId, jsonPrinter: JsonPrinter): StandardRoute = {
- terminate(status, Option(error) filter { _.trim.nonEmpty } map {
- e => Some(ErrorResponse(e.trim, transid))
- } getOrElse None)
+ def terminate(status: StatusCode, error: String)(implicit transid: TransactionId,
+ jsonPrinter: JsonPrinter): StandardRoute = {
+ terminate(status, Option(error) filter { _.trim.nonEmpty } map { e =>
+ Some(ErrorResponse(e.trim, transid))
+ } getOrElse None)
+ }
+
+ def terminate(status: StatusCode, error: Option[ErrorResponse] = None, asJson: Boolean = true)(
+ implicit transid: TransactionId,
+ jsonPrinter: JsonPrinter): StandardRoute = {
+ val errorResponse = error getOrElse response(status)
+ if (asJson) {
+ complete(status, errorResponse)
+ } else {
+ complete(status, s"${errorResponse.error} (code: ${errorResponse.code})")
}
-
- def terminate(status: StatusCode, error: Option[ErrorResponse] = None, asJson: Boolean = true)(
- implicit transid: TransactionId, jsonPrinter: JsonPrinter): StandardRoute = {
- val errorResponse = error getOrElse response(status)
- if (asJson) {
- complete(status, errorResponse)
- } else {
- complete(status, s"${errorResponse.error} (code: ${errorResponse.code})")
+ }
+
+ def response(status: StatusCode)(implicit transid: TransactionId): ErrorResponse = status match {
+ case NotFound => ErrorResponse(Messages.resourceDoesNotExist, transid)
+ case Forbidden => ErrorResponse(Messages.notAuthorizedtoOperateOnResource, transid)
+ case _ => ErrorResponse(status.defaultMessage, transid)
+ }
+
+ implicit val serializer = new RootJsonFormat[ErrorResponse] {
+ def write(er: ErrorResponse) = JsObject("error" -> er.error.toJson, "code" -> er.code.meta.id.toJson)
+
+ def read(v: JsValue) =
+ Try {
+ v.asJsObject.getFields("error", "code") match {
+ case Seq(JsString(error), JsNumber(code)) =>
+ ErrorResponse(error, TransactionId(code))
+ case Seq(JsString(error)) =>
+ ErrorResponse(error, TransactionId.unknown)
}
- }
-
- def response(status: StatusCode)(implicit transid: TransactionId): ErrorResponse = status match {
- case NotFound => ErrorResponse(Messages.resourceDoesNotExist, transid)
- case Forbidden => ErrorResponse(Messages.notAuthorizedtoOperateOnResource, transid)
- case _ => ErrorResponse(status.defaultMessage, transid)
- }
-
- implicit val serializer = new RootJsonFormat[ErrorResponse] {
- def write(er: ErrorResponse) = JsObject(
- "error" -> er.error.toJson,
- "code" -> er.code.meta.id.toJson)
-
- def read(v: JsValue) = Try {
- v.asJsObject.getFields("error", "code") match {
- case Seq(JsString(error), JsNumber(code)) =>
- ErrorResponse(error, TransactionId(code))
- case Seq(JsString(error)) =>
- ErrorResponse(error, TransactionId.unknown)
- }
- } getOrElse deserializationError("error response malformed")
- }
+ } getOrElse deserializationError("error response malformed")
+ }
}
diff --git a/common/scala/src/main/scala/whisk/spi/SpiLoader.scala b/common/scala/src/main/scala/whisk/spi/SpiLoader.scala
index b21dc29..6aa0f6e 100644
--- a/common/scala/src/main/scala/whisk/spi/SpiLoader.scala
+++ b/common/scala/src/main/scala/whisk/spi/SpiLoader.scala
@@ -23,26 +23,29 @@ import com.typesafe.config.ConfigFactory
trait Spi
trait SpiClassResolver {
- /** Resolves the implementation for a given type */
- def getClassNameForType[T : Manifest]: String
+
+ /** Resolves the implementation for a given type */
+ def getClassNameForType[T: Manifest]: String
}
object SpiLoader {
- /**
- * Instantiates an object of the given type.
- *
- * The ClassName to load is resolved via the SpiClassResolver in scode, which defaults to
- * a TypesafeConfig based resolver.
- */
- def get[A <: Spi : Manifest](implicit resolver: SpiClassResolver = TypesafeConfigClassResolver): A = {
- val clazz = Class.forName(resolver.getClassNameForType[A] + "$")
- clazz.getField("MODULE$").get(clazz).asInstanceOf[A]
- }
+
+ /**
+ * Instantiates an object of the given type.
+ *
+ * The ClassName to load is resolved via the SpiClassResolver in scode, which defaults to
+ * a TypesafeConfig based resolver.
+ */
+ def get[A <: Spi: Manifest](implicit resolver: SpiClassResolver = TypesafeConfigClassResolver): A = {
+ val clazz = Class.forName(resolver.getClassNameForType[A] + "$")
+ clazz.getField("MODULE$").get(clazz).asInstanceOf[A]
+ }
}
/** Lookup the classname for the SPI impl based on a key in the provided Config */
object TypesafeConfigClassResolver extends SpiClassResolver {
- private val config = ConfigFactory.load()
+ private val config = ConfigFactory.load()
- override def getClassNameForType[T : Manifest]: String = config.getString("whisk.spi." + manifest[T].runtimeClass.getSimpleName)
+ override def getClassNameForType[T: Manifest]: String =
+ config.getString("whisk.spi." + manifest[T].runtimeClass.getSimpleName)
}
diff --git a/common/scala/src/main/scala/whisk/utils/ExecutionContextFactory.scala b/common/scala/src/main/scala/whisk/utils/ExecutionContextFactory.scala
index 21971b0..88764d3 100644
--- a/common/scala/src/main/scala/whisk/utils/ExecutionContextFactory.scala
+++ b/common/scala/src/main/scala/whisk/utils/ExecutionContextFactory.scala
@@ -26,49 +26,50 @@ import scala.concurrent.duration.FiniteDuration
import scala.util.Try
import akka.actor.ActorSystem
-import akka.pattern.{ after => expire }
+import akka.pattern.{after => expire}
object ExecutionContextFactory {
- // Future.firstCompletedOf has a memory drag bug
- // https://stackoverflow.com/questions/36420697/about-future-firstcompletedof-and-garbage-collect-mechanism
- def firstCompletedOf[T](futures: TraversableOnce[Future[T]])(implicit executor: ExecutionContext): Future[T] = {
- val p = Promise[T]()
- val pref = new java.util.concurrent.atomic.AtomicReference(p)
- val completeFirst: Try[T] => Unit = { result: Try[T] =>
- val promise = pref.getAndSet(null)
- if (promise != null) {
- promise.tryComplete(result)
- }
- }
- futures foreach { _ onComplete completeFirst }
- p.future
+ // Future.firstCompletedOf has a memory drag bug
+ // https://stackoverflow.com/questions/36420697/about-future-firstcompletedof-and-garbage-collect-mechanism
+ def firstCompletedOf[T](futures: TraversableOnce[Future[T]])(implicit executor: ExecutionContext): Future[T] = {
+ val p = Promise[T]()
+ val pref = new java.util.concurrent.atomic.AtomicReference(p)
+ val completeFirst: Try[T] => Unit = { result: Try[T] =>
+ val promise = pref.getAndSet(null)
+ if (promise != null) {
+ promise.tryComplete(result)
+ }
}
+ futures foreach { _ onComplete completeFirst }
+ p.future
+ }
- implicit class FutureExtensions[T](f: Future[T]) {
- def withTimeout(timeout: FiniteDuration, msg: => Throwable)(implicit system: ActorSystem): Future[T] = {
- implicit val ec = system.dispatcher
- firstCompletedOf(Seq(f, expire(timeout, system.scheduler)(Future.failed(msg))))
- }
-
- def withAlternativeAfterTimeout(timeout: FiniteDuration, alt: => Future[T])(implicit system: ActorSystem): Future[T] = {
- implicit val ec = system.dispatcher
- firstCompletedOf(Seq(f, expire(timeout, system.scheduler)(alt)))
- }
+ implicit class FutureExtensions[T](f: Future[T]) {
+ def withTimeout(timeout: FiniteDuration, msg: => Throwable)(implicit system: ActorSystem): Future[T] = {
+ implicit val ec = system.dispatcher
+ firstCompletedOf(Seq(f, expire(timeout, system.scheduler)(Future.failed(msg))))
}
- /**
- * Makes an execution context for Futures using Executors.newCachedThreadPool. From the javadoc:
- *
- * Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads
- * when they are available. These pools will typically improve the performance of programs that execute many
- * short-lived asynchronous tasks. Calls to execute will reuse previously constructed threads if available.
- * If no existing thread is available, a new thread will be created and added to the pool. Threads that have
- * not been used for sixty seconds are terminated and removed from the cache. Thus, a pool that remains idle
- * for long enough will not consume any resources. Note that pools with similar properties but different details
- * (for example, timeout parameters) may be created using ThreadPoolExecutor constructors.
- */
- def makeCachedThreadPoolExecutionContext(): ExecutionContext = {
- ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
+ def withAlternativeAfterTimeout(timeout: FiniteDuration, alt: => Future[T])(
+ implicit system: ActorSystem): Future[T] = {
+ implicit val ec = system.dispatcher
+ firstCompletedOf(Seq(f, expire(timeout, system.scheduler)(alt)))
}
+ }
+
+ /**
+ * Makes an execution context for Futures using Executors.newCachedThreadPool. From the javadoc:
+ *
+ * Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads
+ * when they are available. These pools will typically improve the performance of programs that execute many
+ * short-lived asynchronous tasks. Calls to execute will reuse previously constructed threads if available.
+ * If no existing thread is available, a new thread will be created and added to the pool. Threads that have
+ * not been used for sixty seconds are terminated and removed from the cache. Thus, a pool that remains idle
+ * for long enough will not consume any resources. Note that pools with similar properties but different details
+ * (for example, timeout parameters) may be created using ThreadPoolExecutor constructors.
+ */
+ def makeCachedThreadPoolExecutionContext(): ExecutionContext = {
+ ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
+ }
}
diff --git a/common/scala/src/main/scala/whisk/utils/JsHelpers.scala b/common/scala/src/main/scala/whisk/utils/JsHelpers.scala
index a7bdc00..89f8b1b 100644
--- a/common/scala/src/main/scala/whisk/utils/JsHelpers.scala
+++ b/common/scala/src/main/scala/whisk/utils/JsHelpers.scala
@@ -21,22 +21,23 @@ import spray.json.JsObject
import spray.json.JsValue
object JsHelpers {
- def getFieldPath(js: JsObject, path: List[String]): Option[JsValue] = {
- path match {
- case Nil => Option(js)
- case p :: Nil => js.fields.get(p)
- case p :: tail => js.fields.get(p) match {
- case Some(o: JsObject) => getFieldPath(o, tail)
- case Some(_) => None // head exists but value is not an object so cannot project further
- case None => None // head doesn't exist, cannot project further
- }
+ def getFieldPath(js: JsObject, path: List[String]): Option[JsValue] = {
+ path match {
+ case Nil => Option(js)
+ case p :: Nil => js.fields.get(p)
+ case p :: tail =>
+ js.fields.get(p) match {
+ case Some(o: JsObject) => getFieldPath(o, tail)
+ case Some(_) => None // head exists but value is not an object so cannot project further
+ case None => None // head doesn't exist, cannot project further
}
}
+ }
- def getFieldPath(js: JsObject, path: String*): Option[JsValue] = {
- getFieldPath(js, path.toList)
- }
+ def getFieldPath(js: JsObject, path: String*): Option[JsValue] = {
+ getFieldPath(js, path.toList)
+ }
- def fieldPathExists(js: JsObject, path: List[String]): Boolean = getFieldPath(js, path).isDefined
- def fieldPathExists(js: JsObject, path: String*): Boolean = fieldPathExists(js, path.toList)
+ def fieldPathExists(js: JsObject, path: List[String]): Boolean = getFieldPath(js, path).isDefined
+ def fieldPathExists(js: JsObject, path: String*): Boolean = fieldPathExists(js, path.toList)
}
diff --git a/common/scala/src/main/scala/whisk/utils/Retry.scala b/common/scala/src/main/scala/whisk/utils/Retry.scala
index ace4d97..9a759dd 100644
--- a/common/scala/src/main/scala/whisk/utils/Retry.scala
+++ b/common/scala/src/main/scala/whisk/utils/Retry.scala
@@ -25,25 +25,30 @@ import scala.util.Try
import scala.language.postfixOps
object retry {
- /**
- * Retry a method which returns a value or throws an exception on failure, up to N times,
- * and optionally sleeping up to specified duration between retries.
- *
- * @param fn the method to retry, fn is expected to throw an exception if it fails, else should return a value of type T
- * @param N the maximum number of times to apply fn, must be >= 1
- * @param waitBeforeRetry an option specifying duration to wait before retrying method, will not wait if none given
- * @return the result of fn iff it is successful
- * @throws exception from fn (or an illegal argument exception if N is < 1)
- */
- def apply[T](fn: => T, N: Int = 3, waitBeforeRetry: Option[Duration] = Some(1 millisecond)): T = {
- require(N >= 1, "maximum number of fn applications must be greater than 1")
- waitBeforeRetry map { t => Thread.sleep(t.toMillis) } // initial wait if any
- Try { fn } match {
- case Success(r) => r
- case _ if N > 1 =>
- waitBeforeRetry map { t => Thread.sleep(t.toMillis) }
- retry(fn, N - 1, waitBeforeRetry)
- case Failure(t) => throw t
+
+ /**
+ * Retry a method which returns a value or throws an exception on failure, up to N times,
+ * and optionally sleeping up to specified duration between retries.
+ *
+ * @param fn the method to retry, fn is expected to throw an exception if it fails, else should return a value of type T
+ * @param N the maximum number of times to apply fn, must be >= 1
+ * @param waitBeforeRetry an option specifying duration to wait before retrying method, will not wait if none given
+ * @return the result of fn iff it is successful
+ * @throws exception from fn (or an illegal argument exception if N is < 1)
+ */
+ def apply[T](fn: => T, N: Int = 3, waitBeforeRetry: Option[Duration] = Some(1 millisecond)): T = {
+ require(N >= 1, "maximum number of fn applications must be greater than 1")
+ waitBeforeRetry map { t =>
+ Thread.sleep(t.toMillis)
+ } // initial wait if any
+ Try { fn } match {
+ case Success(r) => r
+ case _ if N > 1 =>
+ waitBeforeRetry map { t =>
+ Thread.sleep(t.toMillis)
}
+ retry(fn, N - 1, waitBeforeRetry)
+ case Failure(t) => throw t
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Actions.scala b/core/controller/src/main/scala/whisk/core/controller/Actions.scala
index d4a59cc..e760c1e 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Actions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Actions.scala
@@ -20,7 +20,7 @@ package whisk.core.controller
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.language.postfixOps
-import scala.util.{ Failure, Success, Try }
+import scala.util.{Failure, Success, Try}
import org.apache.kafka.common.errors.RecordTooLargeException
@@ -58,592 +58,613 @@ import whisk.core.entitlement.Privilege._
* in order to implement the actions API.
*/
object WhiskActionsApi {
- def requiredProperties = Map(WhiskConfig.actionSequenceMaxLimit -> null)
+ def requiredProperties = Map(WhiskConfig.actionSequenceMaxLimit -> null)
- /** Grace period after action timeout limit to poll for result. */
- protected[core] val blockingInvokeGrace = 5 seconds
+ /** Grace period after action timeout limit to poll for result. */
+ protected[core] val blockingInvokeGrace = 5 seconds
- /**
- * Max duration to wait for a blocking activation.
- * This is the default timeout on a POST request.
- */
- protected[core] val maxWaitForBlockingActivation = 60 seconds
+ /**
+ * Max duration to wait for a blocking activation.
+ * This is the default timeout on a POST request.
+ */
+ protected[core] val maxWaitForBlockingActivation = 60 seconds
}
/** A trait implementing the actions API. */
-trait WhiskActionsApi
- extends WhiskCollectionAPI
- with PostActionActivation
- with ReferencedEntities {
- services: WhiskServices =>
-
- protected override val collection = Collection(Collection.ACTIONS)
-
- /** An actor system for timed based futures. */
- protected implicit val actorSystem: ActorSystem
-
- /** Database service to CRUD actions. */
- protected val entityStore: EntityStore
-
- /** Notification service for cache invalidation. */
- protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
-
- /** Database service to get activations. */
- protected val activationStore: ActivationStore
-
- /** Entity normalizer to JSON object. */
- import RestApiCommons.emptyEntityToJsObject
-
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
-
- /**
- * Handles operations on action resources, which encompass these cases:
- *
- * 1. ns/foo -> subject must be authorized for one of { action(ns, *), action(ns, foo) },
- * resource resolves to { action(ns, foo) }
- *
- * 2. ns/bar/foo -> where bar is a package
- * subject must be authorized for one of { package(ns, *), package(ns, bar), action(ns.bar, foo) }
- * resource resolves to { action(ns.bar, foo) }
- *
- * 3. ns/baz/foo -> where baz is a binding to ns'.bar
- * subject must be authorized for one of { package(ns, *), package(ns, baz) }
- * *and* one of { package(ns', *), package(ns', bar), action(ns'.bar, foo) }
- * resource resolves to { action(ns'.bar, foo) }
- *
- * Note that package(ns, xyz) == action(ns.xyz, *) and if subject has rights to package(ns, xyz)
- * then they also have rights to action(ns.xyz, *) since sharing is done at the package level and
- * is not more granular; hence a check on action(ns.xyz, abc) is eschewed.
- *
- * Only list is supported for these resources:
- *
- * 4. ns/bar/ -> where bar is a package
- * subject must be authorized for one of { package(ns, *), package(ns, bar) }
- * resource resolves to { action(ns.bar, *) }
- *
- * 5. ns/baz/ -> where baz is a binding to ns'.bar
- * subject must be authorized for one of { package(ns, *), package(ns, baz) }
- * *and* one of { package(ns', *), package(ns', bar) }
- * resource resolves to { action(ns.bar, *) }
- */
- protected override def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {
- (entityPrefix & entityOps & requestMethod) { (segment, m) =>
- entityname(segment) { outername =>
- pathEnd {
- // matched /namespace/collection/name
- // this is an action in default package, authorize and dispatch
- authorizeAndDispatch(m, user, Resource(ns, collection, Some(outername)))
- } ~ (get & pathSingleSlash) {
- // matched GET /namespace/collection/package-name/
- // list all actions in package iff subject is entitled to READ package
- val resource = Resource(ns, Collection(Collection.PACKAGES), Some(outername))
- onComplete(entitlementProvider.check(user, Privilege.READ, resource)) {
- case Success(_) => listPackageActions(user, FullyQualifiedEntityName(ns, EntityName(outername)))
- case Failure(f) => super.handleEntitlementFailure(f)
- }
- } ~ (entityPrefix & pathEnd) { segment =>
- entityname(segment) { innername =>
- // matched /namespace/collection/package-name/action-name
- // this is an action in a named package
- val packageDocId = FullyQualifiedEntityName(ns, EntityName(outername)).toDocId
- val packageResource = Resource(ns.addPath(EntityName(outername)), collection, Some(innername))
-
- val right = collection.determineRight(m, Some(innername))
- onComplete(entitlementProvider.check(user, right, packageResource)) {
- case Success(_) =>
- getEntity(WhiskPackage, entityStore, packageDocId, Some {
- if (right == Privilege.READ || right == Privilege.ACTIVATE) {
- // need to merge package with action, hence authorize subject for package
- // access (if binding, then subject must be authorized for both the binding
- // and the referenced package)
- //
- // NOTE: it is an error if either the package or the action does not exist,
- // the former manifests as unauthorized and the latter as not found
- mergeActionWithPackageAndDispatch(m, user, EntityName(innername)) _
- } else {
- // these packaged action operations do not need merging with the package,
- // but may not be permitted if this is a binding, or if the subject does
- // not have PUT and DELETE rights to the package itself
- (wp: WhiskPackage) =>
- wp.binding map {
- _ => terminate(BadRequest, Messages.notAllowedOnBinding)
- } getOrElse {
- val actionResource = Resource(wp.fullPath, collection, Some(innername))
- dispatchOp(user, right, actionResource)
- }
- }
- })
- case Failure(f) => super.handleEntitlementFailure(f)
- }
- }
- }
+trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with ReferencedEntities {
+ services: WhiskServices =>
+
+ protected override val collection = Collection(Collection.ACTIONS)
+
+ /** An actor system for timed based futures. */
+ protected implicit val actorSystem: ActorSystem
+
+ /** Database service to CRUD actions. */
+ protected val entityStore: EntityStore
+
+ /** Notification service for cache invalidation. */
+ protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
+
+ /** Database service to get activations. */
+ protected val activationStore: ActivationStore
+
+ /** Entity normalizer to JSON object. */
+ import RestApiCommons.emptyEntityToJsObject
+
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
+
+ /**
+ * Handles operations on action resources, which encompass these cases:
+ *
+ * 1. ns/foo -> subject must be authorized for one of { action(ns, *), action(ns, foo) },
+ * resource resolves to { action(ns, foo) }
+ *
+ * 2. ns/bar/foo -> where bar is a package
+ * subject must be authorized for one of { package(ns, *), package(ns, bar), action(ns.bar, foo) }
+ * resource resolves to { action(ns.bar, foo) }
+ *
+ * 3. ns/baz/foo -> where baz is a binding to ns'.bar
+ * subject must be authorized for one of { package(ns, *), package(ns, baz) }
+ * *and* one of { package(ns', *), package(ns', bar), action(ns'.bar, foo) }
+ * resource resolves to { action(ns'.bar, foo) }
+ *
+ * Note that package(ns, xyz) == action(ns.xyz, *) and if subject has rights to package(ns, xyz)
+ * then they also have rights to action(ns.xyz, *) since sharing is done at the package level and
+ * is not more granular; hence a check on action(ns.xyz, abc) is eschewed.
+ *
+ * Only list is supported for these resources:
+ *
+ * 4. ns/bar/ -> where bar is a package
+ * subject must be authorized for one of { package(ns, *), package(ns, bar) }
+ * resource resolves to { action(ns.bar, *) }
+ *
+ * 5. ns/baz/ -> where baz is a binding to ns'.bar
+ * subject must be authorized for one of { package(ns, *), package(ns, baz) }
+ * *and* one of { package(ns', *), package(ns', bar) }
+ * resource resolves to { action(ns.bar, *) }
+ */
+ protected override def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {
+ (entityPrefix & entityOps & requestMethod) { (segment, m) =>
+ entityname(segment) { outername =>
+ pathEnd {
+ // matched /namespace/collection/name
+ // this is an action in default package, authorize and dispatch
+ authorizeAndDispatch(m, user, Resource(ns, collection, Some(outername)))
+ } ~ (get & pathSingleSlash) {
+ // matched GET /namespace/collection/package-name/
+ // list all actions in package iff subject is entitled to READ package
+ val resource = Resource(ns, Collection(Collection.PACKAGES), Some(outername))
+ onComplete(entitlementProvider.check(user, Privilege.READ, resource)) {
+ case Success(_) => listPackageActions(user, FullyQualifiedEntityName(ns, EntityName(outername)))
+ case Failure(f) => super.handleEntitlementFailure(f)
+ }
+ } ~ (entityPrefix & pathEnd) { segment =>
+ entityname(segment) { innername =>
+ // matched /namespace/collection/package-name/action-name
+ // this is an action in a named package
+ val packageDocId = FullyQualifiedEntityName(ns, EntityName(outername)).toDocId
+ val packageResource = Resource(ns.addPath(EntityName(outername)), collection, Some(innername))
+
+ val right = collection.determineRight(m, Some(innername))
+ onComplete(entitlementProvider.check(user, right, packageResource)) {
+ case Success(_) =>
+ getEntity(WhiskPackage, entityStore, packageDocId, Some {
+ if (right == Privilege.READ || right == Privilege.ACTIVATE) {
+ // need to merge package with action, hence authorize subject for package
+ // access (if binding, then subject must be authorized for both the binding
+ // and the referenced package)
+ //
+ // NOTE: it is an error if either the package or the action does not exist,
+ // the former manifests as unauthorized and the latter as not found
+ mergeActionWithPackageAndDispatch(m, user, EntityName(innername)) _
+ } else {
+ // these packaged action operations do not need merging with the package,
+ // but may not be permitted if this is a binding, or if the subject does
+ // not have PUT and DELETE rights to the package itself
+ (wp: WhiskPackage) =>
+ wp.binding map { _ =>
+ terminate(BadRequest, Messages.notAllowedOnBinding)
+ } getOrElse {
+ val actionResource = Resource(wp.fullPath, collection, Some(innername))
+ dispatchOp(user, right, actionResource)
+ }
+ }
+ })
+ case Failure(f) => super.handleEntitlementFailure(f)
}
+ }
}
+ }
}
-
- /**
- * Creates or updates action if it already exists. The PUT content is deserialized into a WhiskActionPut
- * which is a subset of WhiskAction (it eschews the namespace and entity name since the former is derived
- * from the authenticated user and the latter is derived from the URI). The WhiskActionPut is merged with
- * the existing WhiskAction in the datastore, overriding old values with new values that are defined.
- * Any values not defined in the PUT content are replaced with old values.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskAction as JSON
- * - 400 Bad Request
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- parameter('overwrite ? false) { overwrite =>
- entity(as[WhiskActionPut]) { content =>
- val request = content.resolve(user.namespace)
-
- onComplete(entitleReferencedEntities(user, Privilege.READ, request.exec)) {
- case Success(_) =>
- putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite,
- update(user, request)_, () => { make(user, entityName, request) })
- case Failure(f) =>
- super.handleEntitlementFailure(f)
- }
- }
+ }
+
+ /**
+ * Creates or updates action if it already exists. The PUT content is deserialized into a WhiskActionPut
+ * which is a subset of WhiskAction (it eschews the namespace and entity name since the former is derived
+ * from the authenticated user and the latter is derived from the URI). The WhiskActionPut is merged with
+ * the existing WhiskAction in the datastore, overriding old values with new values that are defined.
+ * Any values not defined in the PUT content are replaced with old values.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskAction as JSON
+ * - 400 Bad Request
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ parameter('overwrite ? false) { overwrite =>
+ entity(as[WhiskActionPut]) { content =>
+ val request = content.resolve(user.namespace)
+
+ onComplete(entitleReferencedEntities(user, Privilege.READ, request.exec)) {
+ case Success(_) =>
+ putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request) _, () => {
+ make(user, entityName, request)
+ })
+ case Failure(f) =>
+ super.handleEntitlementFailure(f)
}
+ }
}
+ }
+
+ /**
+ * Invokes action if it exists. The POST content is deserialized into a Payload and posted
+ * to the loadbalancer.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 Activation as JSON if blocking or just the result JSON iff '&result=true'
+ * - 202 ActivationId as JSON (this is issued on non-blocking activation or blocking activation that times out)
+ * - 404 Not Found
+ * - 502 Bad Gateway
+ * - 500 Internal Server Error
+ */
+ override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ parameter(
+ 'blocking ? false,
+ 'result ? false,
+ 'timeout.as[FiniteDuration] ? WhiskActionsApi.maxWaitForBlockingActivation) { (blocking, result, waitOverride) =>
+ entity(as[Option[JsObject]]) { payload =>
+ getEntity(WhiskAction, entityStore, entityName.toDocId, Some {
+ act: WhiskAction =>
+ // resolve the action --- special case for sequences that may contain components with '_' as default package
+ val action = act.resolve(user.namespace)
+ onComplete(entitleReferencedEntities(user, Privilege.ACTIVATE, Some(action.exec))) {
+ case Success(_) =>
+ val actionWithMergedParams = env.map(action.inherit(_)) getOrElse action
+ val waitForResponse = if (blocking) Some(waitOverride) else None
+ onComplete(invokeAction(user, actionWithMergedParams, payload, waitForResponse, cause = None)) {
+ case Success(Left(activationId)) =>
+ // non-blocking invoke or blocking invoke which got queued instead
+ complete(Accepted, activationId.toJsObject)
+ case Success(Right(activation)) =>
+ val response = if (result) activation.resultAsJson else activation.toExtendedJson
+
+ if (activation.response.isSuccess) {
+ complete(OK, response)
+ } else if (activation.response.isApplicationError) {
+ // actions that result is ApplicationError status are considered a 'success'
+ // and will have an 'error' property in the result - the HTTP status is OK
+ // and clients must check the response status if it exists
+ // NOTE: response status will not exist in the JSON object if ?result == true
+ // and instead clients must check if 'error' is in the JSON
+ // PRESERVING OLD BEHAVIOR and will address defect in separate change
+ complete(BadGateway, response)
+ } else if (activation.response.isContainerError) {
+ complete(BadGateway, response)
+ } else {
+ complete(InternalServerError, response)
+ }
+ case Failure(t: RecordTooLargeException) =>
+ logging.info(this, s"[POST] action payload was too large")
+ terminate(RequestEntityTooLarge)
+ case Failure(RejectRequest(code, message)) =>
+ logging.info(this, s"[POST] action rejected with code $code: $message")
+ terminate(code, message)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[POST] action activation failed: ${t.getMessage}")
+ terminate(InternalServerError)
+ }
- /**
- * Invokes action if it exists. The POST content is deserialized into a Payload and posted
- * to the loadbalancer.
- *
- * Responses are one of (Code, Message)
- * - 200 Activation as JSON if blocking or just the result JSON iff '&result=true'
- * - 202 ActivationId as JSON (this is issued on non-blocking activation or blocking activation that times out)
- * - 404 Not Found
- * - 502 Bad Gateway
- * - 500 Internal Server Error
- */
- override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- parameter('blocking ? false, 'result ? false, 'timeout.as[FiniteDuration] ? WhiskActionsApi.maxWaitForBlockingActivation) { (blocking, result, waitOverride) =>
- entity(as[Option[JsObject]]) { payload =>
- getEntity(WhiskAction, entityStore, entityName.toDocId, Some {
- act: WhiskAction =>
- // resolve the action --- special case for sequences that may contain components with '_' as default package
- val action = act.resolve(user.namespace)
- onComplete(entitleReferencedEntities(user, Privilege.ACTIVATE, Some(action.exec))) {
- case Success(_) =>
- val actionWithMergedParams = env.map(action.inherit(_)) getOrElse action
- val waitForResponse = if (blocking) Some(waitOverride) else None
- onComplete(invokeAction(user, actionWithMergedParams, payload, waitForResponse, cause = None)) {
- case Success(Left(activationId)) =>
- // non-blocking invoke or blocking invoke which got queued instead
- complete(Accepted, activationId.toJsObject)
- case Success(Right(activation)) =>
- val response = if (result) activation.resultAsJson else activation.toExtendedJson
-
- if (activation.response.isSuccess) {
- complete(OK, response)
- } else if (activation.response.isApplicationError) {
- // actions that result is ApplicationError status are considered a 'success'
- // and will have an 'error' property in the result - the HTTP status is OK
- // and clients must check the response status if it exists
- // NOTE: response status will not exist in the JSON object if ?result == true
- // and instead clients must check if 'error' is in the JSON
- // PRESERVING OLD BEHAVIOR and will address defect in separate change
- complete(BadGateway, response)
- } else if (activation.response.isContainerError) {
- complete(BadGateway, response)
- } else {
- complete(InternalServerError, response)
- }
- case Failure(t: RecordTooLargeException) =>
- logging.info(this, s"[POST] action payload was too large")
- terminate(RequestEntityTooLarge)
- case Failure(RejectRequest(code, message)) =>
- logging.info(this, s"[POST] action rejected with code $code: $message")
- terminate(code, message)
- case Failure(t: Throwable) =>
- logging.error(this, s"[POST] action activation failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
-
- case Failure(f) =>
- super.handleEntitlementFailure(f)
- }
- })
+ case Failure(f) =>
+ super.handleEntitlementFailure(f)
}
- }
- }
-
- /**
- * Deletes action.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskAction as JSON
- * - 404 Not Found
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- deleteEntity(WhiskAction, entityStore, entityName.toDocId, (a: WhiskAction) => Future.successful({}))
- }
-
- /**
- * Gets action. The action name is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskAction has JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- getEntity(WhiskAction, entityStore, entityName.toDocId, Some { action: WhiskAction =>
- val mergedAction = env map { action inherit _ } getOrElse action
- complete(OK, mergedAction)
})
+ }
}
-
- /**
- * Gets all actions in a path.
- *
- * Responses are one of (Code, Message)
- * - 200 [] or [WhiskAction as JSON]
- * - 500 Internal Server Error
- */
- override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
- // for consistency, all the collections should support the same list API
- // but because supporting docs on actions is difficult, the API does not
- // offer an option to fetch entities with full docs yet.
- //
- // the complication with actions is that providing docs on actions in
- // package bindings is cannot be do readily done with a couchdb view
- // and would require finding all bindings in namespace and
- // joining the actions explicitly here.
- val docs = false
- parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) {
- (skip, limit, count) =>
- listEntities {
- WhiskAction.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map {
- list =>
- val actions = if (docs) {
- list.right.get map { WhiskAction.serdes.write(_) }
- } else list.left.get
- FilterEntityList.filter(actions, excludePrivate)
- }
- }
+ }
+
+ /**
+ * Deletes action.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskAction as JSON
+ * - 404 Not Found
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ deleteEntity(WhiskAction, entityStore, entityName.toDocId, (a: WhiskAction) => Future.successful({}))
+ }
+
+ /**
+ * Gets action. The action name is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskAction has JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ getEntity(WhiskAction, entityStore, entityName.toDocId, Some { action: WhiskAction =>
+ val mergedAction = env map { action inherit _ } getOrElse action
+ complete(OK, mergedAction)
+ })
+ }
+
+ /**
+ * Gets all actions in a path.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 [] or [WhiskAction as JSON]
+ * - 500 Internal Server Error
+ */
+ override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
+ // for consistency, all the collections should support the same list API
+ // but because supporting docs on actions is difficult, the API does not
+ // offer an option to fetch entities with full docs yet.
+ //
+ // the complication with actions is that providing docs on actions in
+ // package bindings is cannot be do readily done with a couchdb view
+ // and would require finding all bindings in namespace and
+ // joining the actions explicitly here.
+ val docs = false
+ parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) { (skip, limit, count) =>
+ listEntities {
+ WhiskAction.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map { list =>
+ val actions = if (docs) {
+ list.right.get map { WhiskAction.serdes.write(_) }
+ } else list.left.get
+ FilterEntityList.filter(actions, excludePrivate)
}
+ }
}
-
- /** Replaces default namespaces in a vector of components from a sequence with appropriate namespace. */
- private def resolveDefaultNamespace(components: Vector[FullyQualifiedEntityName], user: Identity): Vector[FullyQualifiedEntityName] = {
- // if components are part of the default namespace, they contain `_`; replace it!
- val resolvedComponents = components map { c => FullyQualifiedEntityName(c.path.resolveNamespace(user.namespace), c.name) }
- resolvedComponents
+ }
+
+ /** Replaces default namespaces in a vector of components from a sequence with appropriate namespace. */
+ private def resolveDefaultNamespace(components: Vector[FullyQualifiedEntityName],
+ user: Identity): Vector[FullyQualifiedEntityName] = {
+ // if components are part of the default namespace, they contain `_`; replace it!
+ val resolvedComponents = components map { c =>
+ FullyQualifiedEntityName(c.path.resolveNamespace(user.namespace), c.name)
}
-
- /** Replaces default namespaces in an action sequence with appropriate namespace. */
- private def resolveDefaultNamespace(seq: SequenceExec, user: Identity): SequenceExec = {
- // if components are part of the default namespace, they contain `_`; replace it!
- val resolvedComponents = resolveDefaultNamespace(seq.components, user)
- new SequenceExec(resolvedComponents)
+ resolvedComponents
+ }
+
+ /** Replaces default namespaces in an action sequence with appropriate namespace. */
+ private def resolveDefaultNamespace(seq: SequenceExec, user: Identity): SequenceExec = {
+ // if components are part of the default namespace, they contain `_`; replace it!
+ val resolvedComponents = resolveDefaultNamespace(seq.components, user)
+ new SequenceExec(resolvedComponents)
+ }
+
+ /**
+ * Creates a WhiskAction instance from the PUT request.
+ */
+ private def makeWhiskAction(content: WhiskActionPut, entityName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId) = {
+ val exec = content.exec.get
+ val limits = content.limits map { l =>
+ ActionLimits(l.timeout getOrElse TimeLimit(), l.memory getOrElse MemoryLimit(), l.logs getOrElse LogLimit())
+ } getOrElse ActionLimits()
+ // This is temporary while we are making sequencing directly supported in the controller.
+ // The parameter override allows this to work with Pipecode.code. Any parameters other
+ // than the action sequence itself are discarded and have no effect.
+ // Note: While changing the implementation of sequences, components now store the fully qualified entity names
+ // (which loses the leading "/"). Adding it back while both versions of the code are in place.
+ val parameters = exec match {
+ case seq: SequenceExec =>
+ Parameters("_actions", JsArray(seq.components map { _.qualifiedNameWithLeadingSlash.toJson }))
+ case _ => content.parameters getOrElse Parameters()
}
- /**
- * Creates a WhiskAction instance from the PUT request.
- */
- private def makeWhiskAction(content: WhiskActionPut, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- val exec = content.exec.get
- val limits = content.limits map { l =>
- ActionLimits(
- l.timeout getOrElse TimeLimit(),
- l.memory getOrElse MemoryLimit(),
- l.logs getOrElse LogLimit())
- } getOrElse ActionLimits()
- // This is temporary while we are making sequencing directly supported in the controller.
- // The parameter override allows this to work with Pipecode.code. Any parameters other
- // than the action sequence itself are discarded and have no effect.
- // Note: While changing the implementation of sequences, components now store the fully qualified entity names
- // (which loses the leading "/"). Adding it back while both versions of the code are in place.
- val parameters = exec match {
- case seq: SequenceExec => Parameters("_actions", JsArray(seq.components map { _.qualifiedNameWithLeadingSlash.toJson }))
- case _ => content.parameters getOrElse Parameters()
- }
-
- WhiskAction(
- entityName.path,
- entityName.name,
- exec,
- parameters,
- limits,
- content.version getOrElse SemVer(),
- content.publish getOrElse false,
- (content.annotations getOrElse Parameters()) ++ execAnnotation(exec))
+ WhiskAction(
+ entityName.path,
+ entityName.name,
+ exec,
+ parameters,
+ limits,
+ content.version getOrElse SemVer(),
+ content.publish getOrElse false,
+ (content.annotations getOrElse Parameters()) ++ execAnnotation(exec))
+ }
+
+ /** For a sequence action, gather referenced entities and authorize access. */
+ private def entitleReferencedEntities(user: Identity, right: Privilege, exec: Option[Exec])(
+ implicit transid: TransactionId) = {
+ exec match {
+ case Some(seq: SequenceExec) =>
+ logging.info(this, "checking if sequence components are accessible")
+ entitlementProvider.check(user, right, referencedEntities(seq))
+ case _ => Future.successful(true)
}
-
- /** For a sequence action, gather referenced entities and authorize access. */
- private def entitleReferencedEntities(user: Identity, right: Privilege, exec: Option[Exec])(
- implicit transid: TransactionId) = {
- exec match {
- case Some(seq: SequenceExec) =>
- logging.info(this, "checking if sequence components are accessible")
- entitlementProvider.check(user, right, referencedEntities(seq))
- case _ => Future.successful(true)
+ }
+
+ /** Creates a WhiskAction from PUT content, generating default values where necessary. */
+ private def make(user: Identity, entityName: FullyQualifiedEntityName, content: WhiskActionPut)(
+ implicit transid: TransactionId) = {
+ content.exec map {
+ case seq: SequenceExec =>
+ // check that the sequence conforms to max length and no recursion rules
+ checkSequenceActionLimits(entityName, seq.components) map { _ =>
+ makeWhiskAction(content.replace(seq), entityName)
}
- }
-
- /** Creates a WhiskAction from PUT content, generating default values where necessary. */
- private def make(user: Identity, entityName: FullyQualifiedEntityName, content: WhiskActionPut)(implicit transid: TransactionId) = {
- content.exec map {
- case seq: SequenceExec =>
- // check that the sequence conforms to max length and no recursion rules
- checkSequenceActionLimits(entityName, seq.components) map {
- _ => makeWhiskAction(content.replace(seq), entityName)
- }
- case supportedExec if !supportedExec.deprecated =>
- Future successful makeWhiskAction(content, entityName)
- case deprecatedExec =>
- Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))
-
- } getOrElse Future.failed(RejectRequest(BadRequest, "exec undefined"))
- }
-
- /** Updates a WhiskAction from PUT content, merging old action where necessary. */
- private def update(user: Identity, content: WhiskActionPut)(action: WhiskAction)(implicit transid: TransactionId) = {
- content.exec map {
- case seq: SequenceExec =>
- // check that the sequence conforms to max length and no recursion rules
- checkSequenceActionLimits(FullyQualifiedEntityName(action.namespace, action.name), seq.components) map {
- _ => updateWhiskAction(content.replace(seq), action)
- }
- case supportedExec if !supportedExec.deprecated =>
- Future successful updateWhiskAction(content, action)
- case deprecatedExec =>
- Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))
- } getOrElse {
- if (!action.exec.deprecated) {
- Future successful updateWhiskAction(content, action)
- } else {
- Future failed RejectRequest(BadRequest, runtimeDeprecated(action.exec))
- }
+ case supportedExec if !supportedExec.deprecated =>
+ Future successful makeWhiskAction(content, entityName)
+ case deprecatedExec =>
+ Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))
+
+ } getOrElse Future.failed(RejectRequest(BadRequest, "exec undefined"))
+ }
+
+ /** Updates a WhiskAction from PUT content, merging old action where necessary. */
+ private def update(user: Identity, content: WhiskActionPut)(action: WhiskAction)(implicit transid: TransactionId) = {
+ content.exec map {
+ case seq: SequenceExec =>
+ // check that the sequence conforms to max length and no recursion rules
+ checkSequenceActionLimits(FullyQualifiedEntityName(action.namespace, action.name), seq.components) map { _ =>
+ updateWhiskAction(content.replace(seq), action)
}
+ case supportedExec if !supportedExec.deprecated =>
+ Future successful updateWhiskAction(content, action)
+ case deprecatedExec =>
+ Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))
+ } getOrElse {
+ if (!action.exec.deprecated) {
+ Future successful updateWhiskAction(content, action)
+ } else {
+ Future failed RejectRequest(BadRequest, runtimeDeprecated(action.exec))
+ }
}
-
- /**
- * Updates a WhiskAction instance from the PUT request.
- */
- private def updateWhiskAction(content: WhiskActionPut, action: WhiskAction)(implicit transid: TransactionId) = {
- val limits = content.limits map { l =>
- ActionLimits(l.timeout getOrElse action.limits.timeout, l.memory getOrElse action.limits.memory, l.logs getOrElse action.limits.logs)
- } getOrElse action.limits
-
- // This is temporary while we are making sequencing directly supported in the controller.
- // Actions that are updated with a sequence will have their parameter property overridden.
- // Actions that are updated with non-sequence actions will either set the parameter property according to
- // the content provided, or if that is not defined, and iff the previous version of the action was not a
- // sequence, inherit previous parameters. This is because sequence parameters are special and should not
- // leak to non-sequence actions.
- // If updating an action but not specifying a new exec type, then preserve the previous parameters if the
- // existing type of the action is a sequence (regardless of what parameters may be defined in the content)
- // otherwise, parameters are inferred from the content or previous values.
- // Note: While changing the implementation of sequences, components now store the fully qualified entity names
- // (which loses the leading "/"). Adding it back while both versions of the code are in place. This will disappear completely
- // once the version of sequences with "pipe.js" is removed.
- val parameters = content.exec map {
- case seq: SequenceExec => Parameters("_actions", JsArray(seq.components map { c => JsString("/" + c.toString) }))
- case _ => content.parameters getOrElse {
- action.exec match {
- case seq: SequenceExec => Parameters()
- case _ => action.parameters
- }
- }
- } getOrElse {
- action.exec match {
- case seq: SequenceExec => action.parameters // discard content.parameters
- case _ => content.parameters getOrElse action.parameters
- }
+ }
+
+ /**
+ * Updates a WhiskAction instance from the PUT request.
+ */
+ private def updateWhiskAction(content: WhiskActionPut, action: WhiskAction)(implicit transid: TransactionId) = {
+ val limits = content.limits map { l =>
+ ActionLimits(
+ l.timeout getOrElse action.limits.timeout,
+ l.memory getOrElse action.limits.memory,
+ l.logs getOrElse action.limits.logs)
+ } getOrElse action.limits
+
+ // This is temporary while we are making sequencing directly supported in the controller.
+ // Actions that are updated with a sequence will have their parameter property overridden.
+ // Actions that are updated with non-sequence actions will either set the parameter property according to
+ // the content provided, or if that is not defined, and iff the previous version of the action was not a
+ // sequence, inherit previous parameters. This is because sequence parameters are special and should not
+ // leak to non-sequence actions.
+ // If updating an action but not specifying a new exec type, then preserve the previous parameters if the
+ // existing type of the action is a sequence (regardless of what parameters may be defined in the content)
+ // otherwise, parameters are inferred from the content or previous values.
+ // Note: While changing the implementation of sequences, components now store the fully qualified entity names
+ // (which loses the leading "/"). Adding it back while both versions of the code are in place. This will disappear completely
+ // once the version of sequences with "pipe.js" is removed.
+ val parameters = content.exec map {
+ case seq: SequenceExec =>
+ Parameters("_actions", JsArray(seq.components map { c =>
+ JsString("/" + c.toString)
+ }))
+ case _ =>
+ content.parameters getOrElse {
+ action.exec match {
+ case seq: SequenceExec => Parameters()
+ case _ => action.parameters
+ }
}
-
- val exec = content.exec getOrElse action.exec
-
- WhiskAction(
- action.namespace,
- action.name,
- exec,
- parameters,
- limits,
- content.version getOrElse action.version.upPatch,
- content.publish getOrElse action.publish,
- (content.annotations getOrElse action.annotations) ++ execAnnotation(exec)).
- revision[WhiskAction](action.docinfo.rev)
+ } getOrElse {
+ action.exec match {
+ case seq: SequenceExec => action.parameters // discard content.parameters
+ case _ => content.parameters getOrElse action.parameters
+ }
}
- /**
- * Lists actions in package or binding. The router authorized the subject for the package
- * (if binding, then authorized subject for both the binding and the references package)
- * and iff authorized, this method is reached to lists actions.
- *
- * Note that when listing actions in a binding, the namespace on the actions will be that
- * of the referenced packaged, not the binding.
- */
- private def listPackageActions(user: Identity, pkgName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- // get the package to determine if it is a package or reference
- // (this will set the appropriate namespace), and then list actions
- // NOTE: these fetches are redundant with those from the authorization
- // and should hit the cache to ameliorate the cost; this can be improved
- // but requires communicating back from the authorization service the
- // resolved namespace
- getEntity(WhiskPackage, entityStore, pkgName.toDocId, Some { (wp: WhiskPackage) =>
- val pkgns = wp.binding map { b =>
- logging.info(this, s"list actions in package binding '${wp.name}' -> '$b'")
- b.namespace.addPath(b.name)
- } getOrElse {
- logging.info(this, s"list actions in package '${wp.name}'")
- pkgName.path.addPath(wp.name)
- }
- // list actions in resolved namespace
- // NOTE: excludePrivate is false since the subject is authorize to access
- // the package; in the future, may wish to exclude private actions in a
- // public package instead
- list(user, pkgns, excludePrivate = false)
+ val exec = content.exec getOrElse action.exec
+
+ WhiskAction(
+ action.namespace,
+ action.name,
+ exec,
+ parameters,
+ limits,
+ content.version getOrElse action.version.upPatch,
+ content.publish getOrElse action.publish,
+ (content.annotations getOrElse action.annotations) ++ execAnnotation(exec))
+ .revision[WhiskAction](action.docinfo.rev)
+ }
+
+ /**
+ * Lists actions in package or binding. The router authorized the subject for the package
+ * (if binding, then authorized subject for both the binding and the references package)
+ * and iff authorized, this method is reached to lists actions.
+ *
+ * Note that when listing actions in a binding, the namespace on the actions will be that
+ * of the referenced packaged, not the binding.
+ */
+ private def listPackageActions(user: Identity, pkgName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ // get the package to determine if it is a package or reference
+ // (this will set the appropriate namespace), and then list actions
+ // NOTE: these fetches are redundant with those from the authorization
+ // and should hit the cache to ameliorate the cost; this can be improved
+ // but requires communicating back from the authorization service the
+ // resolved namespace
+ getEntity(WhiskPackage, entityStore, pkgName.toDocId, Some { (wp: WhiskPackage) =>
+ val pkgns = wp.binding map { b =>
+ logging.info(this, s"list actions in package binding '${wp.name}' -> '$b'")
+ b.namespace.addPath(b.name)
+ } getOrElse {
+ logging.info(this, s"list actions in package '${wp.name}'")
+ pkgName.path.addPath(wp.name)
+ }
+ // list actions in resolved namespace
+ // NOTE: excludePrivate is false since the subject is authorize to access
+ // the package; in the future, may wish to exclude private actions in a
+ // public package instead
+ list(user, pkgns, excludePrivate = false)
+ })
+ }
+
+ /**
+ * Constructs a WhiskPackage that is a merger of a package with its packing binding (if any).
+ * This resolves a reference versus an actual package and merge parameters as needed.
+ * Once the package is resolved, the operation is dispatched to the action in the package
+ * namespace.
+ */
+ private def mergeActionWithPackageAndDispatch(method: HttpMethod,
+ user: Identity,
+ action: EntityName,
+ ref: Option[WhiskPackage] = None)(wp: WhiskPackage)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
+ wp.binding map {
+ case b: Binding =>
+ val docid = b.fullyQualifiedName.toDocId
+ logging.info(this, s"fetching package '$docid' for reference")
+ // already checked that subject is authorized for package and binding;
+ // this fetch is redundant but should hit the cache to ameliorate cost
+ getEntity(WhiskPackage, entityStore, docid, Some {
+ mergeActionWithPackageAndDispatch(method, user, action, Some { wp }) _
})
+ } getOrElse {
+ // a subject has implied rights to all resources in a package, so dispatch
+ // operation without further entitlement checks
+ val params = { ref map { _ inherit wp.parameters } getOrElse wp } parameters
+ val ns = wp.namespace.addPath(wp.name) // the package namespace
+ val resource = Resource(ns, collection, Some { action.asString }, Some { params })
+ val right = collection.determineRight(method, resource.entity)
+ logging.info(this, s"merged package parameters and rebased action to '$ns")
+ dispatchOp(user, right, resource)
}
-
- /**
- * Constructs a WhiskPackage that is a merger of a package with its packing binding (if any).
- * This resolves a reference versus an actual package and merge parameters as needed.
- * Once the package is resolved, the operation is dispatched to the action in the package
- * namespace.
- */
- private def mergeActionWithPackageAndDispatch(method: HttpMethod, user: Identity, action: EntityName, ref: Option[WhiskPackage] = None)(wp: WhiskPackage)(
- implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
- wp.binding map {
- case b: Binding =>
- val docid = b.fullyQualifiedName.toDocId
- logging.info(this, s"fetching package '$docid' for reference")
- // already checked that subject is authorized for package and binding;
- // this fetch is redundant but should hit the cache to ameliorate cost
- getEntity(WhiskPackage, entityStore, docid, Some {
- mergeActionWithPackageAndDispatch(method, user, action, Some { wp }) _
- })
- } getOrElse {
- // a subject has implied rights to all resources in a package, so dispatch
- // operation without further entitlement checks
- val params = { ref map { _ inherit wp.parameters } getOrElse wp } parameters
- val ns = wp.namespace.addPath(wp.name) // the package namespace
- val resource = Resource(ns, collection, Some { action.asString }, Some { params })
- val right = collection.determineRight(method, resource.entity)
- logging.info(this, s"merged package parameters and rebased action to '$ns")
- dispatchOp(user, right, resource)
+ }
+
+ /**
+ * Checks that the sequence is not cyclic and that the number of atomic actions in the "inlined" sequence is lower than max allowed.
+ *
+ * @param sequenceAction is the action sequence to check
+ * @param components the components of the sequence
+ */
+ private def checkSequenceActionLimits(
+ sequenceAction: FullyQualifiedEntityName,
+ components: Vector[FullyQualifiedEntityName])(implicit transid: TransactionId): Future[Unit] = {
+ // first checks that current sequence length is allowed
+ // then traverses all actions in the sequence, inlining any that are sequences
+ val future = if (components.size > actionSequenceLimit) {
+ Future.failed(TooManyActionsInSequence())
+ } else if (components.size == 0) {
+ Future.failed(NoComponentInSequence())
+ } else {
+ // resolve the action document id (if it's in a package/binding);
+ // this assumes that entityStore is the same for actions and packages
+ WhiskAction.resolveAction(entityStore, sequenceAction) flatMap { resolvedSeq =>
+ val atomicActionCnt = countAtomicActionsAndCheckCycle(resolvedSeq, components)
+ atomicActionCnt map { count =>
+ logging.debug(this, s"sequence '$sequenceAction' atomic action count $count")
+ if (count > actionSequenceLimit) {
+ throw TooManyActionsInSequence()
+ }
}
+ }
}
- /**
- * Checks that the sequence is not cyclic and that the number of atomic actions in the "inlined" sequence is lower than max allowed.
- *
- * @param sequenceAction is the action sequence to check
- * @param components the components of the sequence
- */
- private def checkSequenceActionLimits(sequenceAction: FullyQualifiedEntityName, components: Vector[FullyQualifiedEntityName])(
- implicit transid: TransactionId): Future[Unit] = {
- // first checks that current sequence length is allowed
- // then traverses all actions in the sequence, inlining any that are sequences
- val future = if (components.size > actionSequenceLimit) {
- Future.failed(TooManyActionsInSequence())
- } else if (components.size == 0) {
- Future.failed(NoComponentInSequence())
- } else {
- // resolve the action document id (if it's in a package/binding);
- // this assumes that entityStore is the same for actions and packages
- WhiskAction.resolveAction(entityStore, sequenceAction) flatMap { resolvedSeq =>
- val atomicActionCnt = countAtomicActionsAndCheckCycle(resolvedSeq, components)
- atomicActionCnt map { count =>
- logging.debug(this, s"sequence '$sequenceAction' atomic action count $count")
- if (count > actionSequenceLimit) {
- throw TooManyActionsInSequence()
- }
- }
- }
- }
-
- future recoverWith {
- case _: TooManyActionsInSequence => Future failed RejectRequest(BadRequest, sequenceIsTooLong)
- case _: NoComponentInSequence => Future failed RejectRequest(BadRequest, sequenceNoComponent)
- case _: SequenceWithCycle => Future failed RejectRequest(BadRequest, sequenceIsCyclic)
- case _: NoDocumentException => Future failed RejectRequest(BadRequest, sequenceComponentNotFound)
- }
+ future recoverWith {
+ case _: TooManyActionsInSequence => Future failed RejectRequest(BadRequest, sequenceIsTooLong)
+ case _: NoComponentInSequence => Future failed RejectRequest(BadRequest, sequenceNoComponent)
+ case _: SequenceWithCycle => Future failed RejectRequest(BadRequest, sequenceIsCyclic)
+ case _: NoDocumentException => Future failed RejectRequest(BadRequest, sequenceComponentNotFound)
}
-
- /**
- * Counts the number of atomic actions in a sequence and checks for potential cycles. The latter is done
- * by inlining any sequence components that are themselves sequences and checking if there if a reference to
- * the given original sequence.
- *
- * @param origSequence the original sequence that is updated/created which generated the checks
- * @param the components of the a sequence to check if they reference the original sequence
- * @return Future with the number of atomic actions in the current sequence or an appropriate error if there is a cycle or a non-existent action reference
- */
- private def countAtomicActionsAndCheckCycle(origSequence: FullyQualifiedEntityName, components: Vector[FullyQualifiedEntityName])(
- implicit transid: TransactionId): Future[Int] = {
- if (components.size > actionSequenceLimit) {
- Future.failed(TooManyActionsInSequence())
- } else {
- // resolve components wrt any package bindings
- val resolvedComponentsFutures = components map { c => WhiskAction.resolveAction(entityStore, c) }
- // traverse the sequence structure by checking each of its components and do the following:
- // 1. check whether any action (sequence or not) referred by the sequence (directly or indirectly)
- // is the same as the original sequence (aka origSequence)
- // 2. count the atomic actions each component has (by "inlining" all sequences)
- val actionCountsFutures = resolvedComponentsFutures map {
- _ flatMap { resolvedComponent =>
- // check whether this component is the same as origSequence
- // this can happen when updating an atomic action to become a sequence
- if (origSequence == resolvedComponent) {
- Future failed SequenceWithCycle()
- } else {
- // check whether component is a sequence or an atomic action
- // if the component does not exist, the future will fail with appropriate error
- WhiskAction.get(entityStore, resolvedComponent.toDocId) flatMap { wskComponent =>
- wskComponent.exec match {
- case SequenceExec(seqComponents) =>
- // sequence action, count the number of atomic actions in this sequence
- countAtomicActionsAndCheckCycle(origSequence, seqComponents)
- case _ => Future successful 1 // atomic action count is one
- }
- }
- }
- }
+ }
+
+ /**
+ * Counts the number of atomic actions in a sequence and checks for potential cycles. The latter is done
+ * by inlining any sequence components that are themselves sequences and checking if there if a reference to
+ * the given original sequence.
+ *
+ * @param origSequence the original sequence that is updated/created which generated the checks
+ * @param the components of the a sequence to check if they reference the original sequence
+ * @return Future with the number of atomic actions in the current sequence or an appropriate error if there is a cycle or a non-existent action reference
+ */
+ private def countAtomicActionsAndCheckCycle(
+ origSequence: FullyQualifiedEntityName,
+ components: Vector[FullyQualifiedEntityName])(implicit transid: TransactionId): Future[Int] = {
+ if (components.size > actionSequenceLimit) {
+ Future.failed(TooManyActionsInSequence())
+ } else {
+ // resolve components wrt any package bindings
+ val resolvedComponentsFutures = components map { c =>
+ WhiskAction.resolveAction(entityStore, c)
+ }
+ // traverse the sequence structure by checking each of its components and do the following:
+ // 1. check whether any action (sequence or not) referred by the sequence (directly or indirectly)
+ // is the same as the original sequence (aka origSequence)
+ // 2. count the atomic actions each component has (by "inlining" all sequences)
+ val actionCountsFutures = resolvedComponentsFutures map {
+ _ flatMap { resolvedComponent =>
+ // check whether this component is the same as origSequence
+ // this can happen when updating an atomic action to become a sequence
+ if (origSequence == resolvedComponent) {
+ Future failed SequenceWithCycle()
+ } else {
+ // check whether component is a sequence or an atomic action
+ // if the component does not exist, the future will fail with appropriate error
+ WhiskAction.get(entityStore, resolvedComponent.toDocId) flatMap { wskComponent =>
+ wskComponent.exec match {
+ case SequenceExec(seqComponents) =>
+ // sequence action, count the number of atomic actions in this sequence
+ countAtomicActionsAndCheckCycle(origSequence, seqComponents)
+ case _ => Future successful 1 // atomic action count is one
+ }
}
- // collapse the futures in one future
- val actionCountsFuture = Future.sequence(actionCountsFutures)
- // sum up all individual action counts per component
- val totalActionCount = actionCountsFuture map { actionCounts => actionCounts.foldLeft(0)(_ + _) }
- totalActionCount
+ }
}
+ }
+ // collapse the futures in one future
+ val actionCountsFuture = Future.sequence(actionCountsFutures)
+ // sum up all individual action counts per component
+ val totalActionCount = actionCountsFuture map { actionCounts =>
+ actionCounts.foldLeft(0)(_ + _)
+ }
+ totalActionCount
}
-
- /**
- * Constructs an "exec" annotation. This is redundant with the exec kind
- * information available in WhiskAction but necessary for some clients which
- * fetch action lists but cannot determine action kinds without fetching them.
- * An alternative is to include the exec in the action list "view" but this
- * will require an API change. So using an annotation instead.
- */
- private def execAnnotation(exec: Exec): Parameters = {
- Parameters(WhiskAction.execFieldName, exec.kind)
- }
-
- /** Max atomic action count allowed for sequences. */
- private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt
-
- implicit val stringToFiniteDuration: Unmarshaller[String, FiniteDuration] = {
- Unmarshaller.strict[String, FiniteDuration] { value =>
- val max = WhiskActionsApi.maxWaitForBlockingActivation.toMillis
-
- Try { value.toInt } match {
- case Success(i) if i > 0 && i <= max => i.milliseconds
- case _ => throw new IllegalArgumentException(Messages.invalidTimeout(WhiskActionsApi.maxWaitForBlockingActivation))
- }
- }
+ }
+
+ /**
+ * Constructs an "exec" annotation. This is redundant with the exec kind
+ * information available in WhiskAction but necessary for some clients which
+ * fetch action lists but cannot determine action kinds without fetching them.
+ * An alternative is to include the exec in the action list "view" but this
+ * will require an API change. So using an annotation instead.
+ */
+ private def execAnnotation(exec: Exec): Parameters = {
+ Parameters(WhiskAction.execFieldName, exec.kind)
+ }
+
+ /** Max atomic action count allowed for sequences. */
+ private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt
+
+ implicit val stringToFiniteDuration: Unmarshaller[String, FiniteDuration] = {
+ Unmarshaller.strict[String, FiniteDuration] { value =>
+ val max = WhiskActionsApi.maxWaitForBlockingActivation.toMillis
+
+ Try { value.toInt } match {
+ case Success(i) if i > 0 && i <= max => i.milliseconds
+ case _ =>
+ throw new IllegalArgumentException(Messages.invalidTimeout(WhiskActionsApi.maxWaitForBlockingActivation))
+ }
}
+ }
}
private case class TooManyActionsInSequence() extends IllegalArgumentException
diff --git a/core/controller/src/main/scala/whisk/core/controller/Activations.scala b/core/controller/src/main/scala/whisk/core/controller/Activations.scala
index 8c162ae..34297f2 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Activations.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Activations.scala
@@ -43,175 +43,205 @@ import whisk.http.ErrorResponse.terminate
import whisk.http.Messages
object WhiskActivationsApi {
- protected[core] val maxActivationLimit = 200
+ protected[core] val maxActivationLimit = 200
}
/** A trait implementing the activations API. */
-trait WhiskActivationsApi
- extends Directives
- with AuthenticatedRouteProvider
- with AuthorizedRouteProvider
- with ReadOps {
-
- protected override val collection = Collection(Collection.ACTIVATIONS)
-
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
-
- /** Database service to GET activations. */
- protected val activationStore: ActivationStore
-
- /** Path to Actions REST API. */
- protected val activationsPath = "activations"
-
- /** Path to activation result and logs. */
- private val resultPath = "result"
- private val logsPath = "logs"
-
- /** Only GET is supported in this API. */
- protected override lazy val entityOps = get
-
- /** Validated entity name as an ActivationId from the matched path segment. */
- protected override def entityname(n: String) = {
- val activationId = Try { ActivationId(n) }
- validate(activationId.isSuccess, activationId match {
- case Failure(DeserializationException(t, _, _)) => t
- case _ => Messages.activationIdIllegal
- }) & extract(_ => n)
+trait WhiskActivationsApi extends Directives with AuthenticatedRouteProvider with AuthorizedRouteProvider with ReadOps {
+
+ protected override val collection = Collection(Collection.ACTIVATIONS)
+
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
+
+ /** Database service to GET activations. */
+ protected val activationStore: ActivationStore
+
+ /** Path to Actions REST API. */
+ protected val activationsPath = "activations"
+
+ /** Path to activation result and logs. */
+ private val resultPath = "result"
+ private val logsPath = "logs"
+
+ /** Only GET is supported in this API. */
+ protected override lazy val entityOps = get
+
+ /** Validated entity name as an ActivationId from the matched path segment. */
+ protected override def entityname(n: String) = {
+ val activationId = Try { ActivationId(n) }
+ validate(activationId.isSuccess, activationId match {
+ case Failure(DeserializationException(t, _, _)) => t
+ case _ => Messages.activationIdIllegal
+ }) & extract(_ => n)
+ }
+
+ /**
+ * Overrides because API allows for GET on /activations and /activations/[result|log] which
+ * would be rejected in the superclass.
+ */
+ override protected def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {
+ (entityPrefix & entityOps & requestMethod) { (segment, m) =>
+ entityname(segment) {
+ // defer rest of the path processing to the fetch operation, which is
+ // the only operation supported on activations that reach the inner route
+ name =>
+ authorizeAndDispatch(m, user, Resource(ns, collection, Some(name)))
+ }
}
-
- /**
- * Overrides because API allows for GET on /activations and /activations/[result|log] which
- * would be rejected in the superclass.
- */
- override protected def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {
- (entityPrefix & entityOps & requestMethod) { (segment, m) =>
- entityname(segment) {
- // defer rest of the path processing to the fetch operation, which is
- // the only operation supported on activations that reach the inner route
- name => authorizeAndDispatch(m, user, Resource(ns, collection, Some(name)))
- }
+ }
+
+ /** Dispatches resource to the proper handler depending on context. */
+ protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = {
+ resource.entity match {
+ case Some(ActivationId(id)) =>
+ op match {
+ case READ => fetch(resource.namespace, id)
+ case _ => reject // should not get here
}
- }
-
- /** Dispatches resource to the proper handler depending on context. */
- protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(implicit transid: TransactionId) = {
- resource.entity match {
- case Some(ActivationId(id)) => op match {
- case READ => fetch(resource.namespace, id)
- case _ => reject // should not get here
- }
- case None => op match {
- case READ => list(resource.namespace)
- case _ => reject // should not get here
- }
+ case None =>
+ op match {
+ case READ => list(resource.namespace)
+ case _ => reject // should not get here
}
}
+ }
+
+ /**
+ * Gets all activations in namespace. Filters by action name if parameter is given.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 [] or [WhiskActivation as JSON]
+ * - 500 Internal Server Error
+ */
+ private def list(namespace: EntityPath)(implicit transid: TransactionId) = {
+ parameter(
+ 'skip ? 0,
+ 'limit ? collection.listLimit,
+ 'count ? false,
+ 'docs ? false,
+ 'name.as[EntityName] ?,
+ 'since.as[Instant] ?,
+ 'upto.as[Instant] ?) { (skip, limit, count, docs, name, since, upto) =>
+ val cappedLimit = if (limit == 0) WhiskActivationsApi.maxActivationLimit else limit
+
+ // regardless of limit, cap at maxActivationLimit (200) records, client must paginate
+ if (cappedLimit <= WhiskActivationsApi.maxActivationLimit) {
+ val activations = name match {
+ case Some(action) =>
+ WhiskActivation.listCollectionByName(
+ activationStore,
+ namespace,
+ action,
+ skip,
+ cappedLimit,
+ docs,
+ since,
+ upto,
+ StaleParameter.UpdateAfter)
+ case None =>
+ WhiskActivation.listCollectionInNamespace(
+ activationStore,
+ namespace,
+ skip,
+ cappedLimit,
+ docs,
+ since,
+ upto,
+ StaleParameter.UpdateAfter)
+ }
- /**
- * Gets all activations in namespace. Filters by action name if parameter is given.
- *
- * Responses are one of (Code, Message)
- * - 200 [] or [WhiskActivation as JSON]
- * - 500 Internal Server Error
- */
- private def list(namespace: EntityPath)(implicit transid: TransactionId) = {
- parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false, 'docs ? false, 'name.as[EntityName]?, 'since.as[Instant]?, 'upto.as[Instant]?) {
- (skip, limit, count, docs, name, since, upto) =>
- val cappedLimit = if (limit == 0) WhiskActivationsApi.maxActivationLimit else limit
-
- // regardless of limit, cap at maxActivationLimit (200) records, client must paginate
- if (cappedLimit <= WhiskActivationsApi.maxActivationLimit) {
- val activations = name match {
- case Some(action) =>
- WhiskActivation.listCollectionByName(activationStore, namespace, action, skip, cappedLimit, docs, since, upto, StaleParameter.UpdateAfter)
- case None =>
- WhiskActivation.listCollectionInNamespace(activationStore, namespace, skip, cappedLimit, docs, since, upto, StaleParameter.UpdateAfter)
- }
-
- listEntities {
- activations map {
- l =>
- if (docs) l.right.get map {
- _.toExtendedJson
- }
- else l.left.get
- }
- }
- } else {
- terminate(BadRequest, Messages.maxActivationLimitExceeded(limit, WhiskActivationsApi.maxActivationLimit))
- }
+ listEntities {
+ activations map { l =>
+ if (docs) l.right.get map {
+ _.toExtendedJson
+ } else l.left.get
+ }
}
+ } else {
+ terminate(BadRequest, Messages.maxActivationLimitExceeded(limit, WhiskActivationsApi.maxActivationLimit))
+ }
}
-
- /**
- * Gets activation. The activation id is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskActivation as JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- private def fetch(namespace: EntityPath, activationId: ActivationId)(implicit transid: TransactionId) = {
- val docid = DocId(WhiskEntity.qualifiedName(namespace, activationId))
- pathEndOrSingleSlash {
- getEntity(WhiskActivation, activationStore, docid, postProcess = Some((activation: WhiskActivation) =>
- complete(activation.toExtendedJson)))
-
- } ~ (pathPrefix(resultPath) & pathEnd) { fetchResponse(docid) } ~
- (pathPrefix(logsPath) & pathEnd) { fetchLogs(docid) }
+ }
+
+ /**
+ * Gets activation. The activation id is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskActivation as JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ private def fetch(namespace: EntityPath, activationId: ActivationId)(implicit transid: TransactionId) = {
+ val docid = DocId(WhiskEntity.qualifiedName(namespace, activationId))
+ pathEndOrSingleSlash {
+ getEntity(
+ WhiskActivation,
+ activationStore,
+ docid,
+ postProcess = Some((activation: WhiskActivation) => complete(activation.toExtendedJson)))
+
+ } ~ (pathPrefix(resultPath) & pathEnd) { fetchResponse(docid) } ~
+ (pathPrefix(logsPath) & pathEnd) { fetchLogs(docid) }
+ }
+
+ /**
+ * Gets activation result. The activation id is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 { result: ..., success: Boolean, statusMessage: String }
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ private def fetchResponse(docid: DocId)(implicit transid: TransactionId) = {
+ getEntityAndProject(
+ WhiskActivation,
+ activationStore,
+ docid,
+ (activation: WhiskActivation) => activation.response.toExtendedJson)
+ }
+
+ /**
+ * Gets activation logs. The activation id is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 { logs: String }
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ private def fetchLogs(docid: DocId)(implicit transid: TransactionId) = {
+ getEntityAndProject(
+ WhiskActivation,
+ activationStore,
+ docid,
+ (activation: WhiskActivation) => activation.logs.toJsonObject)
+ }
+
+ /** Custom unmarshaller for query parameters "name" into valid entity name. */
+ private implicit val stringToEntityName: Unmarshaller[String, EntityName] =
+ Unmarshaller.strict[String, EntityName] { value =>
+ Try { EntityName(value) } match {
+ case Success(e) => e
+ case Failure(t) => throw new IllegalArgumentException(Messages.badEntityName(value))
+ }
}
- /**
- * Gets activation result. The activation id is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 { result: ..., success: Boolean, statusMessage: String }
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- private def fetchResponse(docid: DocId)(implicit transid: TransactionId) = {
- getEntityAndProject(WhiskActivation, activationStore, docid,
- (activation: WhiskActivation) => activation.response.toExtendedJson)
+ /** Custom unmarshaller for query parameters "name" into valid namespace. */
+ private implicit val stringToNamespace: Unmarshaller[String, EntityPath] =
+ Unmarshaller.strict[String, EntityPath] { value =>
+ Try { EntityPath(value) } match {
+ case Success(e) => e
+ case Failure(t) => throw new IllegalArgumentException(Messages.badNamespace(value))
+ }
}
- /**
- * Gets activation logs. The activation id is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 { logs: String }
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- private def fetchLogs(docid: DocId)(implicit transid: TransactionId) = {
- getEntityAndProject(WhiskActivation, activationStore, docid,
- (activation: WhiskActivation) => activation.logs.toJsonObject)
+ /** Custom unmarshaller for query parameters "since" and "upto" into a valid Instant. */
+ private implicit val stringToInstantDeserializer: Unmarshaller[String, Instant] =
+ Unmarshaller.strict[String, Instant] { value =>
+ Try { Instant.ofEpochMilli(value.toLong) } match {
+ case Success(e) => e
+ case Failure(t) => throw new IllegalArgumentException(Messages.badEpoch(value))
+ }
}
-
- /** Custom unmarshaller for query parameters "name" into valid entity name. */
- private implicit val stringToEntityName: Unmarshaller[String, EntityName] =
- Unmarshaller.strict[String, EntityName] { value =>
- Try { EntityName(value) } match {
- case Success(e) => e
- case Failure(t) => throw new IllegalArgumentException(Messages.badEntityName(value))
- }
- }
-
- /** Custom unmarshaller for query parameters "name" into valid namespace. */
- private implicit val stringToNamespace: Unmarshaller[String, EntityPath] =
- Unmarshaller.strict[String, EntityPath] { value =>
- Try { EntityPath(value) } match {
- case Success(e) => e
- case Failure(t) => throw new IllegalArgumentException(Messages.badNamespace(value))
- }
- }
-
- /** Custom unmarshaller for query parameters "since" and "upto" into a valid Instant. */
- private implicit val stringToInstantDeserializer: Unmarshaller[String, Instant] =
- Unmarshaller.strict[String, Instant] { value =>
- Try { Instant.ofEpochMilli(value.toLong) } match {
- case Success(e) => e
- case Failure(t) => throw new IllegalArgumentException(Messages.badEpoch(value))
- }
- }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/ApiUtils.scala b/core/controller/src/main/scala/whisk/core/controller/ApiUtils.scala
index e6f3c51..38d4774 100644
--- a/core/controller/src/main/scala/whisk/core/controller/ApiUtils.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/ApiUtils.scala
@@ -55,50 +55,50 @@ import whisk.http.Messages._
/** An exception to throw inside a Predicate future. */
protected[core] case class RejectRequest(code: StatusCode, message: Option[ErrorResponse]) extends Throwable {
- override def toString = s"RejectRequest($code)" + message.map(" " + _.error).getOrElse("")
+ override def toString = s"RejectRequest($code)" + message.map(" " + _.error).getOrElse("")
}
protected[core] object RejectRequest {
- /** Creates rejection with default message for status code. */
- protected[core] def apply(code: StatusCode)(implicit transid: TransactionId): RejectRequest = {
- RejectRequest(code, Some(ErrorResponse.response(code)(transid)))
- }
- /** Creates rejection with custom message for status code. */
- protected[core] def apply(code: StatusCode, m: String)(implicit transid: TransactionId): RejectRequest = {
- RejectRequest(code, Some(ErrorResponse(m, transid)))
- }
+ /** Creates rejection with default message for status code. */
+ protected[core] def apply(code: StatusCode)(implicit transid: TransactionId): RejectRequest = {
+ RejectRequest(code, Some(ErrorResponse.response(code)(transid)))
+ }
- /** Creates rejection with custom message for status code derived from reason for throwable. */
- protected[core] def apply(code: StatusCode, t: Throwable)(implicit transid: TransactionId): RejectRequest = {
- val reason = t.getMessage
- RejectRequest(code, if (reason != null) reason else "Rejected")
- }
+ /** Creates rejection with custom message for status code. */
+ protected[core] def apply(code: StatusCode, m: String)(implicit transid: TransactionId): RejectRequest = {
+ RejectRequest(code, Some(ErrorResponse(m, transid)))
+ }
+
+ /** Creates rejection with custom message for status code derived from reason for throwable. */
+ protected[core] def apply(code: StatusCode, t: Throwable)(implicit transid: TransactionId): RejectRequest = {
+ val reason = t.getMessage
+ RejectRequest(code, if (reason != null) reason else "Rejected")
+ }
}
protected[controller] object FilterEntityList {
- import WhiskEntity.sharedFieldName
+ import WhiskEntity.sharedFieldName
- /**
- * Filters from a list of entities serialized to JsObjects only those
- * that have the shared field ("publish") equal to true and excludes
- * all others.
- */
- protected[controller] def filter(
- resources: List[JsValue],
- excludePrivate: Boolean,
- additionalFilter: JsObject => Boolean = (_ => true)) = {
- if (excludePrivate) {
- resources filter {
- case obj: JsObject =>
- obj.getFields(sharedFieldName) match {
- case Seq(JsBoolean(true)) => true && additionalFilter(obj) // a shared entity
- case _ => false
- }
- case _ => false // only expecting JsObject instances
- }
- } else resources
- }
+ /**
+ * Filters from a list of entities serialized to JsObjects only those
+ * that have the shared field ("publish") equal to true and excludes
+ * all others.
+ */
+ protected[controller] def filter(resources: List[JsValue],
+ excludePrivate: Boolean,
+ additionalFilter: JsObject => Boolean = (_ => true)) = {
+ if (excludePrivate) {
+ resources filter {
+ case obj: JsObject =>
+ obj.getFields(sharedFieldName) match {
+ case Seq(JsBoolean(true)) => true && additionalFilter(obj) // a shared entity
+ case _ => false
+ }
+ case _ => false // only expecting JsObject instances
+ }
+ } else resources
+ }
}
/**
@@ -106,276 +106,273 @@ protected[controller] object FilterEntityList {
* on an operation and terminate the HTTP request.
*/
package object PostProcess {
- type PostProcessEntity[A] = A => RequestContext => Future[RouteResult]
+ type PostProcessEntity[A] = A => RequestContext => Future[RouteResult]
}
/** A trait for REST APIs that read entities from a datastore */
trait ReadOps extends Directives {
- /** An execution context for futures */
- protected implicit val executionContext: ExecutionContext
+ /** An execution context for futures */
+ protected implicit val executionContext: ExecutionContext
- protected implicit val logging: Logging
+ protected implicit val logging: Logging
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
- /**
- * Get all entities of type A from datastore that match key. Terminates HTTP request.
- *
- * @param factory the factory that can fetch entities of type A from datastore
- * @param datastore the client to the database
- * @param key the key to use to match records in the view, optional, if not defined, use namespace
- * @param view the view to query
- * @param filter a function List[A] => List[A] that filters the results
- *
- * Responses are one of (Code, Message)
- * - 200 entity A [] as JSON []
- * - 500 Internal Server Error
- */
- protected def listEntities(list: Future[List[JsValue]])(implicit transid: TransactionId) = {
- onComplete(list) {
- case Success(entities) =>
- logging.info(this, s"[LIST] entity success")
- complete(OK, entities)
- case Failure(t: Throwable) =>
- logging.error(this, s"[LIST] entity failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
+ /**
+ * Get all entities of type A from datastore that match key. Terminates HTTP request.
+ *
+ * @param factory the factory that can fetch entities of type A from datastore
+ * @param datastore the client to the database
+ * @param key the key to use to match records in the view, optional, if not defined, use namespace
+ * @param view the view to query
+ * @param filter a function List[A] => List[A] that filters the results
+ *
+ * Responses are one of (Code, Message)
+ * - 200 entity A [] as JSON []
+ * - 500 Internal Server Error
+ */
+ protected def listEntities(list: Future[List[JsValue]])(implicit transid: TransactionId) = {
+ onComplete(list) {
+ case Success(entities) =>
+ logging.info(this, s"[LIST] entity success")
+ complete(OK, entities)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[LIST] entity failed: ${t.getMessage}")
+ terminate(InternalServerError)
}
+ }
- /**
- * Gets an entity of type A from datastore. Terminates HTTP request.
- *
- * @param factory the factory that can fetch entity of type A from datastore
- * @param datastore the client to the database
- * @param docid the document id to get
- * @param postProcess an optional continuation to post process the result of the
- * get and terminate the HTTP request directly
- *
- * Responses are one of (Code, Message)
- * - 200 entity A as JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- protected def getEntity[A, Au >: A](
- factory: DocumentFactory[A],
- datastore: ArtifactStore[Au],
- docid: DocId,
- postProcess: Option[PostProcessEntity[A]] = None)(
- implicit transid: TransactionId,
- format: RootJsonFormat[A],
- ma: Manifest[A]) = {
- onComplete(factory.get(datastore, docid)) {
- case Success(entity) =>
- logging.info(this, s"[GET] entity success")
- postProcess map { _(entity) } getOrElse complete(OK, entity)
- case Failure(t: NoDocumentException) =>
- logging.info(this, s"[GET] entity does not exist")
- terminate(NotFound)
- case Failure(t: DocumentTypeMismatchException) =>
- logging.info(this, s"[GET] entity conformance check failed: ${t.getMessage}")
- terminate(Conflict, conformanceMessage)
- case Failure(t: ArtifactStoreException) =>
- logging.info(this, s"[GET] entity unreadable")
- terminate(InternalServerError, t.getMessage)
- case Failure(t: Throwable) =>
- logging.error(this, s"[GET] entity failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
+ /**
+ * Gets an entity of type A from datastore. Terminates HTTP request.
+ *
+ * @param factory the factory that can fetch entity of type A from datastore
+ * @param datastore the client to the database
+ * @param docid the document id to get
+ * @param postProcess an optional continuation to post process the result of the
+ * get and terminate the HTTP request directly
+ *
+ * Responses are one of (Code, Message)
+ * - 200 entity A as JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ protected def getEntity[A, Au >: A](factory: DocumentFactory[A],
+ datastore: ArtifactStore[Au],
+ docid: DocId,
+ postProcess: Option[PostProcessEntity[A]] = None)(implicit transid: TransactionId,
+ format: RootJsonFormat[A],
+ ma: Manifest[A]) = {
+ onComplete(factory.get(datastore, docid)) {
+ case Success(entity) =>
+ logging.info(this, s"[GET] entity success")
+ postProcess map { _(entity) } getOrElse complete(OK, entity)
+ case Failure(t: NoDocumentException) =>
+ logging.info(this, s"[GET] entity does not exist")
+ terminate(NotFound)
+ case Failure(t: DocumentTypeMismatchException) =>
+ logging.info(this, s"[GET] entity conformance check failed: ${t.getMessage}")
+ terminate(Conflict, conformanceMessage)
+ case Failure(t: ArtifactStoreException) =>
+ logging.info(this, s"[GET] entity unreadable")
+ terminate(InternalServerError, t.getMessage)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[GET] entity failed: ${t.getMessage}")
+ terminate(InternalServerError)
}
+ }
- /**
- * Gets an entity of type A from datastore and project fields for response. Terminates HTTP request.
- *
- * @param factory the factory that can fetch entity of type A from datastore
- * @param datastore the client to the database
- * @param docid the document id to get
- * @param project a function A => JSON which projects fields form A
- *
- * Responses are one of (Code, Message)
- * - 200 project(A) as JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- protected def getEntityAndProject[A, Au >: A](
- factory: DocumentFactory[A],
- datastore: ArtifactStore[Au],
- docid: DocId,
- project: A => JsObject)(
- implicit transid: TransactionId,
- format: RootJsonFormat[A],
- ma: Manifest[A]) = {
- onComplete(factory.get(datastore, docid)) {
- case Success(entity) =>
- logging.info(this, s"[PROJECT] entity success")
- complete(OK, project(entity))
- case Failure(t: NoDocumentException) =>
- logging.info(this, s"[PROJECT] entity does not exist")
- terminate(NotFound)
- case Failure(t: DocumentTypeMismatchException) =>
- logging.info(this, s"[PROJECT] entity conformance check failed: ${t.getMessage}")
- terminate(Conflict, conformanceMessage)
- case Failure(t: ArtifactStoreException) =>
- logging.info(this, s"[PROJECT] entity unreadable")
- terminate(InternalServerError, t.getMessage)
- case Failure(t: Throwable) =>
- logging.error(this, s"[PROJECT] entity failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
+ /**
+ * Gets an entity of type A from datastore and project fields for response. Terminates HTTP request.
+ *
+ * @param factory the factory that can fetch entity of type A from datastore
+ * @param datastore the client to the database
+ * @param docid the document id to get
+ * @param project a function A => JSON which projects fields form A
+ *
+ * Responses are one of (Code, Message)
+ * - 200 project(A) as JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ protected def getEntityAndProject[A, Au >: A](
+ factory: DocumentFactory[A],
+ datastore: ArtifactStore[Au],
+ docid: DocId,
+ project: A => JsObject)(implicit transid: TransactionId, format: RootJsonFormat[A], ma: Manifest[A]) = {
+ onComplete(factory.get(datastore, docid)) {
+ case Success(entity) =>
+ logging.info(this, s"[PROJECT] entity success")
+ complete(OK, project(entity))
+ case Failure(t: NoDocumentException) =>
+ logging.info(this, s"[PROJECT] entity does not exist")
+ terminate(NotFound)
+ case Failure(t: DocumentTypeMismatchException) =>
+ logging.info(this, s"[PROJECT] entity conformance check failed: ${t.getMessage}")
+ terminate(Conflict, conformanceMessage)
+ case Failure(t: ArtifactStoreException) =>
+ logging.info(this, s"[PROJECT] entity unreadable")
+ terminate(InternalServerError, t.getMessage)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[PROJECT] entity failed: ${t.getMessage}")
+ terminate(InternalServerError)
}
+ }
}
/** A trait for REST APIs that write entities to a datastore */
trait WriteOps extends Directives {
- /** An execution context for futures */
- protected implicit val executionContext: ExecutionContext
+ /** An execution context for futures */
+ protected implicit val executionContext: ExecutionContext
- protected implicit val logging: Logging
+ protected implicit val logging: Logging
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
- /**
- * A predicate future that completes with true iff the entity should be
- * stored in the datastore. Future should fail otherwise with RejectPut.
- */
- protected type PutPredicate = Future[Boolean]
+ /**
+ * A predicate future that completes with true iff the entity should be
+ * stored in the datastore. Future should fail otherwise with RejectPut.
+ */
+ protected type PutPredicate = Future[Boolean]
- /**
- * Creates or updates an entity of type A in the datastore. First, fetch the entity
- * by id from the datastore (this is required to get the document revision for an update).
- * If the entity does not exist, create it. If it does exist, and 'overwrite' is enabled,
- * update the entity.
- *
- * @param factory the factory that can fetch entity of type A from datastore
- * @param datastore the client to the database
- * @param docid the document id to put
- * @param overwrite updates an existing entity iff overwrite == true
- * @param update a function (A) => Future[A] that updates the existing entity with PUT content
- * @param create a function () => Future[A] that creates a new entity from PUT content
- * @param treatExistsAsConflict if true and document exists but overwrite is not enabled, respond
- * with Conflict else return OK and the existing document
- *
- * Responses are one of (Code, Message)
- * - 200 entity A as JSON
- * - 400 Bad Request
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- protected def putEntity[A, Au >: A](
- factory: DocumentFactory[A],
- datastore: ArtifactStore[Au],
- docid: DocId,
- overwrite: Boolean,
- update: A => Future[A],
- create: () => Future[A],
- treatExistsAsConflict: Boolean = true,
- postProcess: Option[PostProcessEntity[A]] = None)(
- implicit transid: TransactionId,
- format: RootJsonFormat[A],
- notifier: Option[CacheChangeNotification],
- ma: Manifest[A]) = {
- // marker to return an existing doc with status OK rather than conflict if overwrite is false
- case class IdentityPut(self: A) extends Throwable
+ /**
+ * Creates or updates an entity of type A in the datastore. First, fetch the entity
+ * by id from the datastore (this is required to get the document revision for an update).
+ * If the entity does not exist, create it. If it does exist, and 'overwrite' is enabled,
+ * update the entity.
+ *
+ * @param factory the factory that can fetch entity of type A from datastore
+ * @param datastore the client to the database
+ * @param docid the document id to put
+ * @param overwrite updates an existing entity iff overwrite == true
+ * @param update a function (A) => Future[A] that updates the existing entity with PUT content
+ * @param create a function () => Future[A] that creates a new entity from PUT content
+ * @param treatExistsAsConflict if true and document exists but overwrite is not enabled, respond
+ * with Conflict else return OK and the existing document
+ *
+ * Responses are one of (Code, Message)
+ * - 200 entity A as JSON
+ * - 400 Bad Request
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ protected def putEntity[A, Au >: A](factory: DocumentFactory[A],
+ datastore: ArtifactStore[Au],
+ docid: DocId,
+ overwrite: Boolean,
+ update: A => Future[A],
+ create: () => Future[A],
+ treatExistsAsConflict: Boolean = true,
+ postProcess: Option[PostProcessEntity[A]] = None)(
+ implicit transid: TransactionId,
+ format: RootJsonFormat[A],
+ notifier: Option[CacheChangeNotification],
+ ma: Manifest[A]) = {
+ // marker to return an existing doc with status OK rather than conflict if overwrite is false
+ case class IdentityPut(self: A) extends Throwable
- onComplete(factory.get(datastore, docid) flatMap { doc =>
- if (overwrite) {
- logging.info(this, s"[PUT] entity exists, will try to update '$doc'")
- update(doc)
- } else if (treatExistsAsConflict) {
- logging.info(this, s"[PUT] entity exists, but overwrite is not enabled, aborting")
- Future failed RejectRequest(Conflict, "resource already exists")
- } else {
- Future failed IdentityPut(doc)
- }
- } recoverWith {
- case _: NoDocumentException =>
- logging.info(this, s"[PUT] entity does not exist, will try to create it")
- create()
- } flatMap { a =>
- logging.info(this, s"[PUT] entity created/updated, writing back to datastore")
- factory.put(datastore, a) map { _ => a }
- }) {
- case Success(entity) =>
- logging.info(this, s"[PUT] entity success")
- postProcess map { _(entity) } getOrElse complete(OK, entity)
- case Failure(IdentityPut(a)) =>
- logging.info(this, s"[PUT] entity exists, not overwritten")
- complete(OK, a)
- case Failure(t: DocumentConflictException) =>
- logging.info(this, s"[PUT] entity conflict: ${t.getMessage}")
- terminate(Conflict, conflictMessage)
- case Failure(RejectRequest(code, message)) =>
- logging.info(this, s"[PUT] entity rejected with code $code: $message")
- terminate(code, message)
- case Failure(t: DocumentTypeMismatchException) =>
- logging.info(this, s"[PUT] entity conformance check failed: ${t.getMessage}")
- terminate(Conflict, conformanceMessage)
- case Failure(t: ArtifactStoreException) =>
- logging.info(this, s"[PUT] entity unreadable")
- terminate(InternalServerError, t.getMessage)
- case Failure(t: Throwable) =>
- logging.error(this, s"[PUT] entity failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
+ onComplete(factory.get(datastore, docid) flatMap { doc =>
+ if (overwrite) {
+ logging.info(this, s"[PUT] entity exists, will try to update '$doc'")
+ update(doc)
+ } else if (treatExistsAsConflict) {
+ logging.info(this, s"[PUT] entity exists, but overwrite is not enabled, aborting")
+ Future failed RejectRequest(Conflict, "resource already exists")
+ } else {
+ Future failed IdentityPut(doc)
+ }
+ } recoverWith {
+ case _: NoDocumentException =>
+ logging.info(this, s"[PUT] entity does not exist, will try to create it")
+ create()
+ } flatMap { a =>
+ logging.info(this, s"[PUT] entity created/updated, writing back to datastore")
+ factory.put(datastore, a) map { _ =>
+ a
+ }
+ }) {
+ case Success(entity) =>
+ logging.info(this, s"[PUT] entity success")
+ postProcess map { _(entity) } getOrElse complete(OK, entity)
+ case Failure(IdentityPut(a)) =>
+ logging.info(this, s"[PUT] entity exists, not overwritten")
+ complete(OK, a)
+ case Failure(t: DocumentConflictException) =>
+ logging.info(this, s"[PUT] entity conflict: ${t.getMessage}")
+ terminate(Conflict, conflictMessage)
+ case Failure(RejectRequest(code, message)) =>
+ logging.info(this, s"[PUT] entity rejected with code $code: $message")
+ terminate(code, message)
+ case Failure(t: DocumentTypeMismatchException) =>
+ logging.info(this, s"[PUT] entity conformance check failed: ${t.getMessage}")
+ terminate(Conflict, conformanceMessage)
+ case Failure(t: ArtifactStoreException) =>
+ logging.info(this, s"[PUT] entity unreadable")
+ terminate(InternalServerError, t.getMessage)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[PUT] entity failed: ${t.getMessage}")
+ terminate(InternalServerError)
}
+ }
- /**
- * Deletes an entity of type A from datastore.
- * To delete an entity, first fetch the record to identify its revision and then delete it.
- * Terminates HTTP request.
- *
- * @param factory the factory that can fetch entity of type A from datastore
- * @param datastore the client to the database
- * @param docid the document id to delete
- * @param confirm a function (A => Future[Unit]) that confirms the entity is safe to delete (must fail future to abort)
- * or fails the future with an appropriate message
- *
- * Responses are one of (Code, Message)
- * - 200 entity A as JSON
- * - 404 Not Found
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- protected def deleteEntity[A <: WhiskDocument, Au >: A](
- factory: DocumentFactory[A],
- datastore: ArtifactStore[Au],
- docid: DocId,
- confirm: A => Future[Unit],
- postProcess: Option[PostProcessEntity[A]] = None)(
- implicit transid: TransactionId,
- format: RootJsonFormat[A],
- notifier: Option[CacheChangeNotification],
- ma: Manifest[A]) = {
- onComplete(factory.get(datastore, docid) flatMap {
- entity =>
- confirm(entity) flatMap {
- case _ => factory.del(datastore, entity.docinfo) map { _ => entity }
- }
- }) {
- case Success(entity) =>
- logging.info(this, s"[DEL] entity success")
- postProcess map { _(entity) } getOrElse complete(OK, entity)
- case Failure(t: NoDocumentException) =>
- logging.info(this, s"[DEL] entity does not exist")
- terminate(NotFound)
- case Failure(t: DocumentConflictException) =>
- logging.info(this, s"[DEL] entity conflict: ${t.getMessage}")
- terminate(Conflict, conflictMessage)
- case Failure(RejectRequest(code, message)) =>
- logging.info(this, s"[DEL] entity rejected with code $code: $message")
- terminate(code, message)
- case Failure(t: DocumentTypeMismatchException) =>
- logging.info(this, s"[DEL] entity conformance check failed: ${t.getMessage}")
- terminate(Conflict, conformanceMessage)
- case Failure(t: ArtifactStoreException) =>
- logging.info(this, s"[DEL] entity unreadable")
- terminate(InternalServerError, t.getMessage)
- case Failure(t: Throwable) =>
- logging.error(this, s"[DEL] entity failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
+ /**
+ * Deletes an entity of type A from datastore.
+ * To delete an entity, first fetch the record to identify its revision and then delete it.
+ * Terminates HTTP request.
+ *
+ * @param factory the factory that can fetch entity of type A from datastore
+ * @param datastore the client to the database
+ * @param docid the document id to delete
+ * @param confirm a function (A => Future[Unit]) that confirms the entity is safe to delete (must fail future to abort)
+ * or fails the future with an appropriate message
+ *
+ * Responses are one of (Code, Message)
+ * - 200 entity A as JSON
+ * - 404 Not Found
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ protected def deleteEntity[A <: WhiskDocument, Au >: A](factory: DocumentFactory[A],
+ datastore: ArtifactStore[Au],
+ docid: DocId,
+ confirm: A => Future[Unit],
+ postProcess: Option[PostProcessEntity[A]] = None)(
+ implicit transid: TransactionId,
+ format: RootJsonFormat[A],
+ notifier: Option[CacheChangeNotification],
+ ma: Manifest[A]) = {
+ onComplete(factory.get(datastore, docid) flatMap { entity =>
+ confirm(entity) flatMap {
+ case _ =>
+ factory.del(datastore, entity.docinfo) map { _ =>
+ entity
+ }
+ }
+ }) {
+ case Success(entity) =>
+ logging.info(this, s"[DEL] entity success")
+ postProcess map { _(entity) } getOrElse complete(OK, entity)
+ case Failure(t: NoDocumentException) =>
+ logging.info(this, s"[DEL] entity does not exist")
+ terminate(NotFound)
+ case Failure(t: DocumentConflictException) =>
+ logging.info(this, s"[DEL] entity conflict: ${t.getMessage}")
+ terminate(Conflict, conflictMessage)
+ case Failure(RejectRequest(code, message)) =>
+ logging.info(this, s"[DEL] entity rejected with code $code: $message")
+ terminate(code, message)
+ case Failure(t: DocumentTypeMismatchException) =>
+ logging.info(this, s"[DEL] entity conformance check failed: ${t.getMessage}")
+ terminate(Conflict, conformanceMessage)
+ case Failure(t: ArtifactStoreException) =>
+ logging.info(this, s"[DEL] entity unreadable")
+ terminate(InternalServerError, t.getMessage)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[DEL] entity failed: ${t.getMessage}")
+ terminate(InternalServerError)
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Authenticate.scala b/core/controller/src/main/scala/whisk/core/controller/Authenticate.scala
index ea3db36..cf8899f 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Authenticate.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Authenticate.scala
@@ -36,47 +36,49 @@ import whisk.core.entity.Secret
import whisk.core.entity.UUID
object Authenticate {
- /** Required properties for this component */
- def requiredProperties = WhiskAuthStore.requiredProperties
+
+ /** Required properties for this component */
+ def requiredProperties = WhiskAuthStore.requiredProperties
}
/** A trait to validate basic auth credentials */
trait Authenticate {
- protected implicit val executionContext: ExecutionContext
- protected implicit val logging: Logging
+ protected implicit val executionContext: ExecutionContext
+ protected implicit val logging: Logging
- /** Database service to lookup credentials */
- protected val authStore: AuthStore
+ /** Database service to lookup credentials */
+ protected val authStore: AuthStore
- /**
- * Validates credentials against the authentication database; may be used in
- * authentication directive.
- */
- def validateCredentials(credentials: Option[BasicHttpCredentials])(implicit transid: TransactionId): Future[Option[Identity]] = {
- credentials flatMap { pw =>
- Try {
- // authkey deserialization is wrapped in a try to guard against malformed values
- val authkey = AuthKey(UUID(pw.username), Secret(pw.password))
- logging.info(this, s"authenticate: ${authkey.uuid}")
- val future = Identity.get(authStore, authkey) map { result =>
- if (authkey == result.authkey) {
- logging.info(this, s"authentication valid")
- Some(result)
- } else {
- logging.info(this, s"authentication not valid")
- None
- }
- } recover {
- case _: NoDocumentException | _: IllegalArgumentException =>
- logging.info(this, s"authentication not valid")
- None
- }
- future onFailure { case t => logging.error(this, s"authentication error: $t") }
- future
- }.toOption
- } getOrElse {
- credentials.foreach(_ => logging.info(this, s"credentials are malformed"))
- Future.successful(None)
+ /**
+ * Validates credentials against the authentication database; may be used in
+ * authentication directive.
+ */
+ def validateCredentials(credentials: Option[BasicHttpCredentials])(
+ implicit transid: TransactionId): Future[Option[Identity]] = {
+ credentials flatMap { pw =>
+ Try {
+ // authkey deserialization is wrapped in a try to guard against malformed values
+ val authkey = AuthKey(UUID(pw.username), Secret(pw.password))
+ logging.info(this, s"authenticate: ${authkey.uuid}")
+ val future = Identity.get(authStore, authkey) map { result =>
+ if (authkey == result.authkey) {
+ logging.info(this, s"authentication valid")
+ Some(result)
+ } else {
+ logging.info(this, s"authentication not valid")
+ None
+ }
+ } recover {
+ case _: NoDocumentException | _: IllegalArgumentException =>
+ logging.info(this, s"authentication not valid")
+ None
}
+ future onFailure { case t => logging.error(this, s"authentication error: $t") }
+ future
+ }.toOption
+ } getOrElse {
+ credentials.foreach(_ => logging.info(this, s"credentials are malformed"))
+ Future.successful(None)
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala b/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala
index e9b481f..69a63f4 100644
--- a/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala
@@ -33,24 +33,25 @@ import whisk.core.entity.Identity
/** A common trait for secured routes */
trait AuthenticatedRoute {
- /** An execution context for futures */
- protected implicit val executionContext: ExecutionContext
-
- /** Creates HTTP BasicAuth handler */
- def basicAuth[A](verify: Option[BasicHttpCredentials] => Future[Option[A]]) = {
- authenticateOrRejectWithChallenge[BasicHttpCredentials, A] { creds =>
- verify(creds).map {
- case Some(t) => AuthenticationResult.success(t)
- case None => AuthenticationResult.failWithChallenge(HttpChallenges.basic("OpenWhisk secure realm"))
- }
- }
+ /** An execution context for futures */
+ protected implicit val executionContext: ExecutionContext
+
+ /** Creates HTTP BasicAuth handler */
+ def basicAuth[A](verify: Option[BasicHttpCredentials] => Future[Option[A]]) = {
+ authenticateOrRejectWithChallenge[BasicHttpCredentials, A] { creds =>
+ verify(creds).map {
+ case Some(t) => AuthenticationResult.success(t)
+ case None => AuthenticationResult.failWithChallenge(HttpChallenges.basic("OpenWhisk secure realm"))
+ }
}
+ }
- /** Validates credentials against database of subjects */
- protected def validateCredentials(credentials: Option[BasicHttpCredentials])(implicit transid: TransactionId): Future[Option[Identity]]
+ /** Validates credentials against database of subjects */
+ protected def validateCredentials(credentials: Option[BasicHttpCredentials])(
+ implicit transid: TransactionId): Future[Option[Identity]]
}
/** A trait for authenticated routes. */
trait AuthenticatedRouteProvider {
- def routes(user: Identity)(implicit transid: TransactionId): Route
+ def routes(user: Identity)(implicit transid: TransactionId): Route
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/AuthorizedRouteDispatcher.scala b/core/controller/src/main/scala/whisk/core/controller/AuthorizedRouteDispatcher.scala
index 7812a37..41c5932 100644
--- a/core/controller/src/main/scala/whisk/core/controller/AuthorizedRouteDispatcher.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/AuthorizedRouteDispatcher.scala
@@ -44,79 +44,71 @@ import whisk.http.Messages
/** A trait for routes that require entitlement checks. */
trait BasicAuthorizedRouteProvider extends Directives {
- /** An execution context for futures */
- protected implicit val executionContext: ExecutionContext
+ /** An execution context for futures */
+ protected implicit val executionContext: ExecutionContext
- /** An entitlement service to check access rights. */
- protected val entitlementProvider: EntitlementProvider
+ /** An entitlement service to check access rights. */
+ protected val entitlementProvider: EntitlementProvider
- /** The collection type for this trait. */
- protected val collection: Collection
+ /** The collection type for this trait. */
+ protected val collection: Collection
- /** Route directives for API. The methods that are supported on the collection. */
- protected lazy val collectionOps = pathEndOrSingleSlash & get
+ /** Route directives for API. The methods that are supported on the collection. */
+ protected lazy val collectionOps = pathEndOrSingleSlash & get
- /** Route directives for API. The path prefix that identifies entity handlers. */
- protected lazy val entityPrefix = pathPrefix(Segment)
+ /** Route directives for API. The path prefix that identifies entity handlers. */
+ protected lazy val entityPrefix = pathPrefix(Segment)
- /** Route directives for API. The methods that are supported on entities. */
- protected lazy val entityOps = get
+ /** Route directives for API. The methods that are supported on entities. */
+ protected lazy val entityOps = get
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
- /** Checks entitlement and dispatches to handler if authorized. */
- protected def authorizeAndDispatch(
- method: HttpMethod,
- user: Identity,
- resource: Resource)(
- implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
- val right = collection.determineRight(method, resource.entity)
+ /** Checks entitlement and dispatches to handler if authorized. */
+ protected def authorizeAndDispatch(method: HttpMethod, user: Identity, resource: Resource)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
+ val right = collection.determineRight(method, resource.entity)
- onComplete(entitlementProvider.check(user, right, resource)) {
- case Success(_) => dispatchOp(user, right, resource)
- case Failure(t) => handleEntitlementFailure(t)
- }
- }
-
- protected def handleEntitlementFailure(failure: Throwable)(
- implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
- failure match {
- case (r: RejectRequest) => terminate(r.code, r.message)
- case t => terminate(InternalServerError)
- }
+ onComplete(entitlementProvider.check(user, right, resource)) {
+ case Success(_) => dispatchOp(user, right, resource)
+ case Failure(t) => handleEntitlementFailure(t)
}
+ }
- /** Dispatches resource to the proper handler depending on context. */
- protected def dispatchOp(
- user: Identity,
- op: Privilege,
- resource: Resource)(
- implicit transid: TransactionId): RequestContext => Future[RouteResult]
-
- /** Extracts namespace for user from the matched path segment. */
- protected def namespace(user: Identity, ns: String) = {
- validate(isNamespace(ns), {
- if (ns.length > EntityName.ENTITY_NAME_MAX_LENGTH) {
- Messages.entityNameTooLong(
- SizeError(
- namespaceDescriptionForSizeError,
- ns.length.B,
- EntityName.ENTITY_NAME_MAX_LENGTH.B))
- } else {
- Messages.namespaceIllegal
- }
- }) & extract(_ => EntityPath(if (EntityPath(ns) == EntityPath.DEFAULT) user.namespace.asString else ns))
+ protected def handleEntitlementFailure(failure: Throwable)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
+ failure match {
+ case (r: RejectRequest) => terminate(r.code, r.message)
+ case t => terminate(InternalServerError)
}
+ }
+
+ /** Dispatches resource to the proper handler depending on context. */
+ protected def dispatchOp(user: Identity, op: Privilege, resource: Resource)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult]
+
+ /** Extracts namespace for user from the matched path segment. */
+ protected def namespace(user: Identity, ns: String) = {
+ validate(
+ isNamespace(ns), {
+ if (ns.length > EntityName.ENTITY_NAME_MAX_LENGTH) {
+ Messages.entityNameTooLong(
+ SizeError(namespaceDescriptionForSizeError, ns.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))
+ } else {
+ Messages.namespaceIllegal
+ }
+ }) & extract(_ => EntityPath(if (EntityPath(ns) == EntityPath.DEFAULT) user.namespace.asString else ns))
+ }
- /** Validates entity name from the matched path segment. */
- protected val namespaceDescriptionForSizeError = "Namespace"
+ /** Validates entity name from the matched path segment. */
+ protected val namespaceDescriptionForSizeError = "Namespace"
- /** Extracts the HTTP method which is used to determine privilege for resource. */
- protected val requestMethod = extract(_.request.method)
+ /** Extracts the HTTP method which is used to determine privilege for resource. */
+ protected val requestMethod = extract(_.request.method)
- /** Confirms that a path segment is a valid namespace. Used to reject invalid namespaces. */
- protected def isNamespace(n: String) = Try { EntityPath(n) } isSuccess
+ /** Confirms that a path segment is a valid namespace. Used to reject invalid namespaces. */
+ protected def isNamespace(n: String) = Try { EntityPath(n) } isSuccess
}
/**
@@ -125,51 +117,51 @@ trait BasicAuthorizedRouteProvider extends Directives {
*/
trait AuthorizedRouteProvider extends BasicAuthorizedRouteProvider {
- /**
- * Route directives for API.
- * The default path prefix for the collection is one of
- * '_/collection-path' matching an implicit namespace, or
- * 'explicit-namespace/collection-path'.
- */
- protected lazy val collectionPrefix = pathPrefix((EntityPath.DEFAULT.toString.r | Segment) / collection.path)
-
- /** Route directives for API. The methods that are supported on entities. */
- override protected lazy val entityOps = put | get | delete | post
-
- /**
- * Common REST API for Whisk Entities. Defines all the routes handled by this API. They are:
- *
- * GET namespace/entities[/] -- list all entities in namespace
- * GET namespace/entities/name -- fetch entity by name from namespace
- * PUT namespace/entities/name -- create or update entity by name from namespace with content
- * DEL namespace/entities/name -- remove entity by name form namespace
- * POST namespace/entities/name -- "activate" entity by name from namespace with content
- *
- * @param user the authenticated user for this route
- */
- def routes(user: Identity)(implicit transid: TransactionId) = {
- collectionPrefix { segment =>
- namespace(user, segment) { ns =>
- (collectionOps & requestMethod) {
- // matched /namespace/collection
- authorizeAndDispatch(_, user, Resource(ns, collection, None))
- } ~ innerRoutes(user, ns)
- }
- }
+ /**
+ * Route directives for API.
+ * The default path prefix for the collection is one of
+ * '_/collection-path' matching an implicit namespace, or
+ * 'explicit-namespace/collection-path'.
+ */
+ protected lazy val collectionPrefix = pathPrefix((EntityPath.DEFAULT.toString.r | Segment) / collection.path)
+
+ /** Route directives for API. The methods that are supported on entities. */
+ override protected lazy val entityOps = put | get | delete | post
+
+ /**
+ * Common REST API for Whisk Entities. Defines all the routes handled by this API. They are:
+ *
+ * GET namespace/entities[/] -- list all entities in namespace
+ * GET namespace/entities/name -- fetch entity by name from namespace
+ * PUT namespace/entities/name -- create or update entity by name from namespace with content
+ * DEL namespace/entities/name -- remove entity by name form namespace
+ * POST namespace/entities/name -- "activate" entity by name from namespace with content
+ *
+ * @param user the authenticated user for this route
+ */
+ def routes(user: Identity)(implicit transid: TransactionId) = {
+ collectionPrefix { segment =>
+ namespace(user, segment) { ns =>
+ (collectionOps & requestMethod) {
+ // matched /namespace/collection
+ authorizeAndDispatch(_, user, Resource(ns, collection, None))
+ } ~ innerRoutes(user, ns)
+ }
}
-
- /**
- * Handles the inner routes of the collection. This allows customizing nested resources.
- */
- protected def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {
- (entityPrefix & entityOps & requestMethod) { (segment, m) =>
- // matched /namespace/collection/entity
- (entityname(segment) & pathEnd) {
- name => authorizeAndDispatch(m, user, Resource(ns, collection, Some(name)))
- }
- }
+ }
+
+ /**
+ * Handles the inner routes of the collection. This allows customizing nested resources.
+ */
+ protected def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {
+ (entityPrefix & entityOps & requestMethod) { (segment, m) =>
+ // matched /namespace/collection/entity
+ (entityname(segment) & pathEnd) { name =>
+ authorizeAndDispatch(m, user, Resource(ns, collection, Some(name)))
+ }
}
+ }
- /** Extracts and validates entity name from the matched path segment. */
- protected def entityname(segment: String): Directive1[String]
+ /** Extracts and validates entity name from the matched path segment. */
+ protected def entityname(segment: String): Directive1[String]
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Backend.scala b/core/controller/src/main/scala/whisk/core/controller/Backend.scala
index a6c9a54..cd74b8d 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Backend.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Backend.scala
@@ -26,15 +26,16 @@ import whisk.core.loadBalancer.LoadBalancer
* A trait which defines a few services which a whisk microservice may rely on.
*/
trait WhiskServices {
- /** Whisk configuration object. */
- protected val whiskConfig: WhiskConfig
- /** An entitlement service to check access rights. */
- protected val entitlementProvider: EntitlementProvider
+ /** Whisk configuration object. */
+ protected val whiskConfig: WhiskConfig
- /** A generator for new activation ids. */
- protected val activationIdFactory: ActivationIdGenerator
+ /** An entitlement service to check access rights. */
+ protected val entitlementProvider: EntitlementProvider
- /** A load balancing service that launches invocations. */
- protected val loadBalancer: LoadBalancer
+ /** A generator for new activation ids. */
+ protected val activationIdFactory: ActivationIdGenerator
+
+ /** A load balancing service that launches invocations. */
+ protected val loadBalancer: LoadBalancer
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Controller.scala b/core/controller/src/main/scala/whisk/core/controller/Controller.scala
index 4037e0d..85f5662 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Controller.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Controller.scala
@@ -19,7 +19,7 @@ package whisk.core.controller
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
-import scala.util.{ Failure, Success }
+import scala.util.{Failure, Success}
import akka.actor._
import akka.actor.ActorSystem
@@ -68,74 +68,76 @@ import whisk.http.BasicRasService
* @param verbosity logging verbosity
* @param executionContext Scala runtime support for concurrent operations
*/
-class Controller(
- override val instance: InstanceId,
- runtimes: Runtimes,
- implicit val whiskConfig: WhiskConfig,
- implicit val actorSystem: ActorSystem,
- implicit val materializer: ActorMaterializer,
- implicit val logging: Logging)
+class Controller(override val instance: InstanceId,
+ runtimes: Runtimes,
+ implicit val whiskConfig: WhiskConfig,
+ implicit val actorSystem: ActorSystem,
+ implicit val materializer: ActorMaterializer,
+ implicit val logging: Logging)
extends BasicRasService {
- override val numberOfInstances = whiskConfig.controllerInstances.toInt
-
- TransactionId.controller.mark(this, LoggingMarkers.CONTROLLER_STARTUP(instance.toInt), s"starting controller instance ${instance.toInt}")
-
- /**
- * A Route in Akka is technically a function taking a RequestContext as a parameter.
- *
- * The "~" Akka DSL operator composes two independent Routes, building a routing tree structure.
- * @see http://doc.akka.io/docs/akka-http/current/scala/http/routing-dsl/routes.html#composing-routes
- */
- override def routes(implicit transid: TransactionId): Route = {
- super.routes ~ {
- (pathEndOrSingleSlash & get) {
- complete(info)
- }
- } ~ apiV1.routes ~ swagger.swaggerRoutes ~ internalInvokerHealth
+ override val numberOfInstances = whiskConfig.controllerInstances.toInt
+
+ TransactionId.controller.mark(
+ this,
+ LoggingMarkers.CONTROLLER_STARTUP(instance.toInt),
+ s"starting controller instance ${instance.toInt}")
+
+ /**
+ * A Route in Akka is technically a function taking a RequestContext as a parameter.
+ *
+ * The "~" Akka DSL operator composes two independent Routes, building a routing tree structure.
+ * @see http://doc.akka.io/docs/akka-http/current/scala/http/routing-dsl/routes.html#composing-routes
+ */
+ override def routes(implicit transid: TransactionId): Route = {
+ super.routes ~ {
+ (pathEndOrSingleSlash & get) {
+ complete(info)
+ }
+ } ~ apiV1.routes ~ swagger.swaggerRoutes ~ internalInvokerHealth
+ }
+
+ // initialize datastores
+ private implicit val authStore = WhiskAuthStore.datastore(whiskConfig)
+ private implicit val entityStore = WhiskEntityStore.datastore(whiskConfig)
+ private implicit val activationStore = WhiskActivationStore.datastore(whiskConfig)
+ private implicit val cacheChangeNotification = Some(new CacheChangeNotification {
+ val remoteCacheInvalidaton = new RemoteCacheInvalidation(whiskConfig, "controller", instance)
+ override def apply(k: CacheKey) = remoteCacheInvalidaton.notifyOtherInstancesAboutInvalidation(k)
+ })
+
+ // initialize backend services
+ private implicit val loadBalancer = new LoadBalancerService(whiskConfig, instance, entityStore)
+ private implicit val entitlementProvider = new LocalEntitlementProvider(whiskConfig, loadBalancer)
+ private implicit val activationIdFactory = new ActivationIdGenerator {}
+
+ // register collections
+ Collection.initialize(entityStore)
+
+ /** The REST APIs. */
+ implicit val controllerInstance = instance
+ private val apiV1 = new RestAPIVersion(whiskConfig, "api", "v1")
+ private val swagger = new SwaggerDocs(Uri.Path.Empty, "infoswagger.json")
+
+ /**
+ * Handles GET /invokers URI.
+ *
+ * @return JSON of invoker health
+ */
+ private val internalInvokerHealth = {
+ implicit val executionContext = actorSystem.dispatcher
+
+ (path("invokers") & get) {
+ complete {
+ loadBalancer.allInvokers.map(_.map {
+ case (instance, state) => s"invoker${instance.toInt}" -> state.asString
+ }.toMap.toJson.asJsObject)
+ }
}
+ }
- // initialize datastores
- private implicit val authStore = WhiskAuthStore.datastore(whiskConfig)
- private implicit val entityStore = WhiskEntityStore.datastore(whiskConfig)
- private implicit val activationStore = WhiskActivationStore.datastore(whiskConfig)
- private implicit val cacheChangeNotification = Some(new CacheChangeNotification {
- val remoteCacheInvalidaton = new RemoteCacheInvalidation(whiskConfig, "controller", instance)
- override def apply(k: CacheKey) = remoteCacheInvalidaton.notifyOtherInstancesAboutInvalidation(k)
- })
-
- // initialize backend services
- private implicit val loadBalancer = new LoadBalancerService(whiskConfig, instance, entityStore)
- private implicit val entitlementProvider = new LocalEntitlementProvider(whiskConfig, loadBalancer)
- private implicit val activationIdFactory = new ActivationIdGenerator {}
-
- // register collections
- Collection.initialize(entityStore)
-
- /** The REST APIs. */
- implicit val controllerInstance = instance
- private val apiV1 = new RestAPIVersion(whiskConfig, "api", "v1")
- private val swagger = new SwaggerDocs(Uri.Path.Empty, "infoswagger.json")
-
- /**
- * Handles GET /invokers URI.
- *
- * @return JSON of invoker health
- */
- private val internalInvokerHealth = {
- implicit val executionContext = actorSystem.dispatcher
-
- (path("invokers") & get) {
- complete {
- loadBalancer.allInvokers.map(_.map {
- case (instance, state) => s"invoker${instance.toInt}" -> state.asString
- }.toMap.toJson.asJsObject)
- }
- }
- }
-
- // controller top level info
- private val info = Controller.info(whiskConfig, runtimes, List(apiV1.basepath()))
+ // controller top level info
+ private val info = Controller.info(whiskConfig, runtimes, List(apiV1.basepath()))
}
/**
@@ -143,58 +145,66 @@ class Controller(
*/
object Controller {
- // requiredProperties is a Map whose keys define properties that must be bound to
- // a value, and whose values are default values. A null value in the Map means there is
- // no default value specified, so it must appear in the properties file
- def requiredProperties = Map(WhiskConfig.controllerInstances -> null) ++
- ExecManifest.requiredProperties ++
- RestApiCommons.requiredProperties ++
- LoadBalancerService.requiredProperties ++
- EntitlementProvider.requiredProperties
-
- private def info(config: WhiskConfig, runtimes: Runtimes, apis: List[String]) = JsObject(
- "description" -> "OpenWhisk".toJson,
- "support" -> JsObject(
- "github" -> "https://github.com/apache/incubator-openwhisk/issues".toJson,
- "slack" -> "http://slack.openwhisk.org".toJson),
- "api_paths" -> apis.toJson,
- "limits" -> JsObject(
- "actions_per_minute" -> config.actionInvokePerMinuteLimit.toInt.toJson,
- "triggers_per_minute" -> config.triggerFirePerMinuteLimit.toInt.toJson,
- "concurrent_actions" -> config.actionInvokeConcurrentLimit.toInt.toJson),
- "runtimes" -> runtimes.toJson)
-
- def main(args: Array[String]): Unit = {
- implicit val actorSystem = ActorSystem("controller-actor-system")
- implicit val logger = new AkkaLogging(akka.event.Logging.getLogger(actorSystem, this))
-
- // extract configuration data from the environment
- val config = new WhiskConfig(requiredProperties)
- val port = config.servicePort.toInt
-
- // if deploying multiple instances (scale out), must pass the instance number as the
- require(args.length >= 1, "controller instance required")
- val instance = args(0).toInt
-
- def abort() = {
- logger.error(this, "Bad configuration, cannot start.")
- actorSystem.terminate()
- Await.result(actorSystem.whenTerminated, 30.seconds)
- sys.exit(1)
- }
-
- if (!config.isValid) {
- abort()
- }
-
- ExecManifest.initialize(config) match {
- case Success(_) =>
- val controller = new Controller(InstanceId(instance), ExecManifest.runtimesManifest, config, actorSystem, ActorMaterializer.create(actorSystem), logger)
- BasicHttpService.startService(controller.route, port)(actorSystem, controller.materializer)
-
- case Failure(t) =>
- logger.error(this, s"Invalid runtimes manifest: $t")
- abort()
- }
+ // requiredProperties is a Map whose keys define properties that must be bound to
+ // a value, and whose values are default values. A null value in the Map means there is
+ // no default value specified, so it must appear in the properties file
+ def requiredProperties =
+ Map(WhiskConfig.controllerInstances -> null) ++
+ ExecManifest.requiredProperties ++
+ RestApiCommons.requiredProperties ++
+ LoadBalancerService.requiredProperties ++
+ EntitlementProvider.requiredProperties
+
+ private def info(config: WhiskConfig, runtimes: Runtimes, apis: List[String]) =
+ JsObject(
+ "description" -> "OpenWhisk".toJson,
+ "support" -> JsObject(
+ "github" -> "https://github.com/apache/incubator-openwhisk/issues".toJson,
+ "slack" -> "http://slack.openwhisk.org".toJson),
+ "api_paths" -> apis.toJson,
+ "limits" -> JsObject(
+ "actions_per_minute" -> config.actionInvokePerMinuteLimit.toInt.toJson,
+ "triggers_per_minute" -> config.triggerFirePerMinuteLimit.toInt.toJson,
+ "concurrent_actions" -> config.actionInvokeConcurrentLimit.toInt.toJson),
+ "runtimes" -> runtimes.toJson)
+
+ def main(args: Array[String]): Unit = {
+ implicit val actorSystem = ActorSystem("controller-actor-system")
+ implicit val logger = new AkkaLogging(akka.event.Logging.getLogger(actorSystem, this))
+
+ // extract configuration data from the environment
+ val config = new WhiskConfig(requiredProperties)
+ val port = config.servicePort.toInt
+
+ // if deploying multiple instances (scale out), must pass the instance number as the
+ require(args.length >= 1, "controller instance required")
+ val instance = args(0).toInt
+
+ def abort() = {
+ logger.error(this, "Bad configuration, cannot start.")
+ actorSystem.terminate()
+ Await.result(actorSystem.whenTerminated, 30.seconds)
+ sys.exit(1)
+ }
+
+ if (!config.isValid) {
+ abort()
+ }
+
+ ExecManifest.initialize(config) match {
+ case Success(_) =>
+ val controller = new Controller(
+ InstanceId(instance),
+ ExecManifest.runtimesManifest,
+ config,
+ actorSystem,
+ ActorMaterializer.create(actorSystem),
+ logger)
+ BasicHttpService.startService(controller.route, port)(actorSystem, controller.materializer)
+
+ case Failure(t) =>
+ logger.error(this, s"Invalid runtimes manifest: $t")
+ abort()
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Entities.scala b/core/controller/src/main/scala/whisk/core/controller/Entities.scala
index fbf75b3..4693ac7 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Entities.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Entities.scala
@@ -42,26 +42,27 @@ import whisk.http.Messages
protected[controller] trait ValidateRequestSize extends Directives {
- protected def validateSize(check: => Option[SizeError])(
- implicit tid: TransactionId, jsonPrinter: JsonPrinter) = new Directive0 {
- override def tapply(f: Unit => Route) = {
- check map {
- case e: SizeError => terminate(RequestEntityTooLarge, Messages.entityTooBig(e))
- } getOrElse f(None)
- }
+ protected def validateSize(check: => Option[SizeError])(implicit tid: TransactionId, jsonPrinter: JsonPrinter) =
+ new Directive0 {
+ override def tapply(f: Unit => Route) = {
+ check map {
+ case e: SizeError => terminate(RequestEntityTooLarge, Messages.entityTooBig(e))
+ } getOrElse f(None)
+ }
}
- /** Checks if request entity is within allowed length range. */
- protected def isWhithinRange(length: Long) = {
- if (length <= allowedActivationEntitySize) {
- None
- } else Some {
- SizeError(fieldDescriptionForSizeError, length.B, allowedActivationEntitySize.B)
- }
- }
-
- protected val allowedActivationEntitySize: Long = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT.toBytes
- protected val fieldDescriptionForSizeError = "Request"
+ /** Checks if request entity is within allowed length range. */
+ protected def isWhithinRange(length: Long) = {
+ if (length <= allowedActivationEntitySize) {
+ None
+ } else
+ Some {
+ SizeError(fieldDescriptionForSizeError, length.B, allowedActivationEntitySize.B)
+ }
+ }
+
+ protected val allowedActivationEntitySize: Long = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT.toBytes
+ protected val fieldDescriptionForSizeError = "Request"
}
/** A trait implementing the basic operations on WhiskEntities in support of the various APIs. */
@@ -73,81 +74,87 @@ trait WhiskCollectionAPI
with ReadOps
with WriteOps {
- /** The core collections require backend services to be injected in this trait. */
- services: WhiskServices =>
-
- /** Creates an entity, or updates an existing one, in namespace. Terminates HTTP request. */
- protected def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId): RequestContext => Future[RouteResult]
-
- /** Activates entity. Examples include invoking an action, firing a trigger, enabling/disabling a rule. */
- protected def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId): RequestContext => Future[RouteResult]
-
- /** Removes entity from namespace. Terminates HTTP request. */
- protected def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId): RequestContext => Future[RouteResult]
-
- /** Gets entity from namespace. Terminates HTTP request. */
- protected def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId): RequestContext => Future[RouteResult]
-
- /** Gets all entities from namespace. If necessary filter only entities that are shared. Terminates HTTP request. */
- protected def list(user: Identity, path: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId): RequestContext => Future[RouteResult]
-
- /** Indicates if listing entities in collection requires filtering out private entities. */
- protected val listRequiresPrivateEntityFilter = false // currently supported on PACKAGES only
-
- /** Dispatches resource to the proper handler depending on context. */
- protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(implicit transid: TransactionId) = {
- resource.entity match {
- case Some(EntityName(name)) => op match {
- case READ => fetch(user, FullyQualifiedEntityName(resource.namespace, name), resource.env)
- case PUT =>
- entity(as[LimitedWhiskEntityPut]) { e =>
- validateSize(e.isWithinSizeLimits)(transid, RestApiCommons.jsonDefaultResponsePrinter) {
- create(user, FullyQualifiedEntityName(resource.namespace, name))
- }
- }
- case ACTIVATE =>
- extract(_.request.entity.contentLengthOption) { length =>
- validateSize(isWhithinRange(length.getOrElse(0)))(transid, RestApiCommons.jsonDefaultResponsePrinter) {
- activate(user, FullyQualifiedEntityName(resource.namespace, name), resource.env)
- }
- }
-
- case DELETE => remove(user, FullyQualifiedEntityName(resource.namespace, name))
- case _ => reject
+ /** The core collections require backend services to be injected in this trait. */
+ services: WhiskServices =>
+
+ /** Creates an entity, or updates an existing one, in namespace. Terminates HTTP request. */
+ protected def create(user: Identity, entityName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult]
+
+ /** Activates entity. Examples include invoking an action, firing a trigger, enabling/disabling a rule. */
+ protected def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult]
+
+ /** Removes entity from namespace. Terminates HTTP request. */
+ protected def remove(user: Identity, entityName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult]
+
+ /** Gets entity from namespace. Terminates HTTP request. */
+ protected def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult]
+
+ /** Gets all entities from namespace. If necessary filter only entities that are shared. Terminates HTTP request. */
+ protected def list(user: Identity, path: EntityPath, excludePrivate: Boolean)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult]
+
+ /** Indicates if listing entities in collection requires filtering out private entities. */
+ protected val listRequiresPrivateEntityFilter = false // currently supported on PACKAGES only
+
+ /** Dispatches resource to the proper handler depending on context. */
+ protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = {
+ resource.entity match {
+ case Some(EntityName(name)) =>
+ op match {
+ case READ => fetch(user, FullyQualifiedEntityName(resource.namespace, name), resource.env)
+ case PUT =>
+ entity(as[LimitedWhiskEntityPut]) { e =>
+ validateSize(e.isWithinSizeLimits)(transid, RestApiCommons.jsonDefaultResponsePrinter) {
+ create(user, FullyQualifiedEntityName(resource.namespace, name))
+ }
}
- case None => op match {
- case READ =>
- // the entitlement service will authorize any subject to list PACKAGES
- // in any namespace regardless of ownership but the list operation CANNOT
- // produce all entities in the requested namespace UNLESS the subject is
- // entitled to them which for now means they own the namespace. If the
- // subject does not own the namespace, then exclude packages that are private
- val excludePrivate = listRequiresPrivateEntityFilter && resource.namespace.root != user.namespace
- logging.info(this, s"[LIST] exclude private entities: required == $excludePrivate")
- list(user, resource.namespace, excludePrivate)
-
- case _ => reject
+ case ACTIVATE =>
+ extract(_.request.entity.contentLengthOption) { length =>
+ validateSize(isWhithinRange(length.getOrElse(0)))(transid, RestApiCommons.jsonDefaultResponsePrinter) {
+ activate(user, FullyQualifiedEntityName(resource.namespace, name), resource.env)
+ }
}
- }
- }
- /** Validates entity name from the matched path segment. */
- protected val segmentDescriptionForSizeError = "Name segement"
-
- protected override final def entityname(s: String) = {
- validate(isEntity(s), {
- if (s.length > EntityName.ENTITY_NAME_MAX_LENGTH) {
- Messages.entityNameTooLong(
- SizeError(
- segmentDescriptionForSizeError,
- s.length.B,
- EntityName.ENTITY_NAME_MAX_LENGTH.B))
- } else {
- Messages.entityNameIllegal
- }
- }) & extract(_ => s)
+ case DELETE => remove(user, FullyQualifiedEntityName(resource.namespace, name))
+ case _ => reject
+ }
+ case None =>
+ op match {
+ case READ =>
+ // the entitlement service will authorize any subject to list PACKAGES
+ // in any namespace regardless of ownership but the list operation CANNOT
+ // produce all entities in the requested namespace UNLESS the subject is
+ // entitled to them which for now means they own the namespace. If the
+ // subject does not own the namespace, then exclude packages that are private
+ val excludePrivate = listRequiresPrivateEntityFilter && resource.namespace.root != user.namespace
+ logging.info(this, s"[LIST] exclude private entities: required == $excludePrivate")
+ list(user, resource.namespace, excludePrivate)
+
+ case _ => reject
+ }
}
+ }
+
+ /** Validates entity name from the matched path segment. */
+ protected val segmentDescriptionForSizeError = "Name segement"
+
+ protected override final def entityname(s: String) = {
+ validate(
+ isEntity(s), {
+ if (s.length > EntityName.ENTITY_NAME_MAX_LENGTH) {
+ Messages.entityNameTooLong(
+ SizeError(segmentDescriptionForSizeError, s.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))
+ } else {
+ Messages.entityNameIllegal
+ }
+ }) & extract(_ => s)
+ }
- /** Confirms that a path segment is a valid entity name. Used to reject invalid entity names. */
- protected final def isEntity(n: String) = Try { EntityName(n) } isSuccess
+ /** Confirms that a path segment is a valid entity name. Used to reject invalid entity names. */
+ protected final def isEntity(n: String) = Try { EntityName(n) } isSuccess
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Namespaces.scala b/core/controller/src/main/scala/whisk/core/controller/Namespaces.scala
index d14e3b7..61420a9 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Namespaces.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Namespaces.scala
@@ -51,85 +51,87 @@ trait WhiskNamespacesApi
with BasicAuthorizedRouteProvider
with ReadOps {
- protected override val collection = Collection(Collection.NAMESPACES)
+ protected override val collection = Collection(Collection.NAMESPACES)
- /** Database service to lookup entities in a namespace. */
- protected val entityStore: EntityStore
+ /** Database service to lookup entities in a namespace. */
+ protected val entityStore: EntityStore
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
- /**
- * Rest API for managing namespaces. Defines all the routes handled by this API. They are:
- *
- * GET namespaces[/] -- gets namespaces for authenticated user
- * GET namespaces/_[/] -- gets all entities in implicit namespace
- * GET namespaces/namespace[/] -- gets all entities in explicit namespace
- *
- * @param user the authenticated user for this route
- */
- override def routes(user: Identity)(implicit transid: TransactionId) = {
- pathPrefix(collection.path) {
- (collectionOps & requestMethod) { m =>
- getNamespaces(user)
- } ~ (entityOps & entityPrefix & pathEndOrSingleSlash & requestMethod) { (segment, m) =>
- namespace(user, segment) { ns =>
- val resource = Resource(ns, collection, None)
- authorizeAndDispatch(m, user, resource)
- }
- }
+ /**
+ * Rest API for managing namespaces. Defines all the routes handled by this API. They are:
+ *
+ * GET namespaces[/] -- gets namespaces for authenticated user
+ * GET namespaces/_[/] -- gets all entities in implicit namespace
+ * GET namespaces/namespace[/] -- gets all entities in explicit namespace
+ *
+ * @param user the authenticated user for this route
+ */
+ override def routes(user: Identity)(implicit transid: TransactionId) = {
+ pathPrefix(collection.path) {
+ (collectionOps & requestMethod) { m =>
+ getNamespaces(user)
+ } ~ (entityOps & entityPrefix & pathEndOrSingleSlash & requestMethod) { (segment, m) =>
+ namespace(user, segment) { ns =>
+ val resource = Resource(ns, collection, None)
+ authorizeAndDispatch(m, user, resource)
}
+ }
}
+ }
- /**
- * GET / -- gets namespaces for authenticated user
- * GET /namespace -- gets all entities in namespace
- *
- * The namespace of the resource is derived from the authenticated user. The
- * resource entity name, if it is defined, may be a different namespace.
- */
- protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(implicit transid: TransactionId) = {
- resource.entity match {
- case None if op == READ => getAllInNamespace(resource.namespace)
- case _ => reject // should not get here
- }
+ /**
+ * GET / -- gets namespaces for authenticated user
+ * GET /namespace -- gets all entities in namespace
+ *
+ * The namespace of the resource is derived from the authenticated user. The
+ * resource entity name, if it is defined, may be a different namespace.
+ */
+ protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = {
+ resource.entity match {
+ case None if op == READ => getAllInNamespace(resource.namespace)
+ case _ => reject // should not get here
}
+ }
- /**
- * Gets all entities in namespace.
- *
- * Responses are one of (Code, Message)
- * - 200 Map [ String (collection name), List[EntitySummary] ] as JSON
- * - 500 Internal Server Error
- */
- private def getAllInNamespace(namespace: EntityPath)(implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
- onComplete(listEntitiesInNamespace(entityStore, namespace, false)) {
- case Success(entities) => {
- complete(OK, Namespaces.emptyNamespace ++ entities - WhiskActivation.collectionName)
- }
- case Failure(t) =>
- logging.error(this, s"[GET] namespaces failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
+ /**
+ * Gets all entities in namespace.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 Map [ String (collection name), List[EntitySummary] ] as JSON
+ * - 500 Internal Server Error
+ */
+ private def getAllInNamespace(namespace: EntityPath)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
+ onComplete(listEntitiesInNamespace(entityStore, namespace, false)) {
+ case Success(entities) => {
+ complete(OK, Namespaces.emptyNamespace ++ entities - WhiskActivation.collectionName)
+ }
+ case Failure(t) =>
+ logging.error(this, s"[GET] namespaces failed: ${t.getMessage}")
+ terminate(InternalServerError)
}
+ }
- /**
- * Gets namespaces for subject from entitlement service.
- *
- * Responses are one of (Code, Message)
- * - 200 [ Namespaces (as String) ] as JSON
- * - 401 Unauthorized
- * - 500 Internal Server Error
- */
- private def getNamespaces(user: Identity)(implicit transid: TransactionId) = {
- complete(OK, List(user.namespace))
- }
+ /**
+ * Gets namespaces for subject from entitlement service.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 [ Namespaces (as String) ] as JSON
+ * - 401 Unauthorized
+ * - 500 Internal Server Error
+ */
+ private def getNamespaces(user: Identity)(implicit transid: TransactionId) = {
+ complete(OK, List(user.namespace))
+ }
}
object Namespaces {
- val emptyNamespace = Map(
- WhiskAction.collectionName -> List(),
- WhiskPackage.collectionName -> List(),
- WhiskRule.collectionName -> List(),
- WhiskTrigger.collectionName -> List())
+ val emptyNamespace = Map(
+ WhiskAction.collectionName -> List(),
+ WhiskPackage.collectionName -> List(),
+ WhiskRule.collectionName -> List(),
+ WhiskTrigger.collectionName -> List())
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Packages.scala b/core/controller/src/main/scala/whisk/core/controller/Packages.scala
index cca510e..36139ba 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Packages.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Packages.scala
@@ -40,283 +40,313 @@ import whisk.http.ErrorResponse.terminate
import whisk.http.Messages
trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
- services: WhiskServices =>
+ services: WhiskServices =>
- protected override val collection = Collection(Collection.PACKAGES)
+ protected override val collection = Collection(Collection.PACKAGES)
- /** Database service to CRUD packages. */
- protected val entityStore: EntityStore
+ /** Database service to CRUD packages. */
+ protected val entityStore: EntityStore
- /** Notification service for cache invalidation. */
- protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
+ /** Notification service for cache invalidation. */
+ protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
- /** Route directives for API. The methods that are supported on packages. */
- protected override lazy val entityOps = put | get | delete
+ /** Route directives for API. The methods that are supported on packages. */
+ protected override lazy val entityOps = put | get | delete
- /** Must exclude any private packages when listing those in a namespace unless owned by subject. */
- protected override val listRequiresPrivateEntityFilter = true
+ /** Must exclude any private packages when listing those in a namespace unless owned by subject. */
+ protected override val listRequiresPrivateEntityFilter = true
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
- /**
- * Creates or updates package/binding if it already exists. The PUT content is deserialized into a
- * WhiskPackagePut which is a subset of WhiskPackage (it eschews the namespace and entity name since
- * the former is derived from the authenticated user and the latter is derived from the URI). If the
- * binding property is defined, creates or updates a package binding as long as resource is already a
- * binding.
- *
- * The WhiskPackagePut is merged with the existing WhiskPackage in the datastore, overriding old values
- * with new values that are defined. Any values not defined in the PUT content are replaced with old values.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskPackage as JSON
- * - 400 Bad Request
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- parameter('overwrite ? false) { overwrite =>
- entity(as[WhiskPackagePut]) { content =>
- val request = content.resolve(entityName.namespace)
+ /**
+ * Creates or updates package/binding if it already exists. The PUT content is deserialized into a
+ * WhiskPackagePut which is a subset of WhiskPackage (it eschews the namespace and entity name since
+ * the former is derived from the authenticated user and the latter is derived from the URI). If the
+ * binding property is defined, creates or updates a package binding as long as resource is already a
+ * binding.
+ *
+ * The WhiskPackagePut is merged with the existing WhiskPackage in the datastore, overriding old values
+ * with new values that are defined. Any values not defined in the PUT content are replaced with old values.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskPackage as JSON
+ * - 400 Bad Request
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ parameter('overwrite ? false) { overwrite =>
+ entity(as[WhiskPackagePut]) { content =>
+ val request = content.resolve(entityName.namespace)
- request.binding.map { b => logging.info(this, "checking if package is accessible") }
- val referencedentities = referencedEntities(request)
+ request.binding.map { b =>
+ logging.info(this, "checking if package is accessible")
+ }
+ val referencedentities = referencedEntities(request)
- onComplete(entitlementProvider.check(user, Privilege.READ, referencedentities)) {
- case Success(_) =>
- putEntity(WhiskPackage, entityStore, entityName.toDocId, overwrite,
- update(request) _, () => create(request, entityName))
- case Failure(f) =>
- rewriteEntitlementFailure(f)
- }
- }
+ onComplete(entitlementProvider.check(user, Privilege.READ, referencedentities)) {
+ case Success(_) =>
+ putEntity(
+ WhiskPackage,
+ entityStore,
+ entityName.toDocId,
+ overwrite,
+ update(request) _,
+ () => create(request, entityName))
+ case Failure(f) =>
+ rewriteEntitlementFailure(f)
}
+ }
}
+ }
- /**
- * Activating a package is not supported. This method is not permitted and is not reachable.
- *
- * Responses are one of (Code, Message)
- * - 405 Not Allowed
- */
- override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- logging.error(this, "activate is not permitted on packages")
- reject
- }
+ /**
+ * Activating a package is not supported. This method is not permitted and is not reachable.
+ *
+ * Responses are one of (Code, Message)
+ * - 405 Not Allowed
+ */
+ override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ logging.error(this, "activate is not permitted on packages")
+ reject
+ }
- /**
- * Deletes package/binding. If a package, may only be deleted if there are no entities in the package.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskPackage as JSON
- * - 404 Not Found
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- deleteEntity(WhiskPackage, entityStore, entityName.toDocId, (wp: WhiskPackage) => {
- wp.binding map {
- // this is a binding, it is safe to remove
- _ => Future.successful({})
- } getOrElse {
- // may only delete a package if all its ingredients are deleted already
- WhiskAction.listCollectionInNamespace(entityStore, wp.namespace.addPath(wp.name), skip = 0, limit = 0) flatMap {
- case Left(list) if (list.size != 0) =>
- Future failed {
- RejectRequest(Conflict, s"Package not empty (contains ${list.size} ${if (list.size == 1) "entity" else "entities"})")
- }
- case _ => Future.successful({})
- }
- }
- })
- }
+ /**
+ * Deletes package/binding. If a package, may only be deleted if there are no entities in the package.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskPackage as JSON
+ * - 404 Not Found
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ deleteEntity(
+ WhiskPackage,
+ entityStore,
+ entityName.toDocId,
+ (wp: WhiskPackage) => {
+ wp.binding map {
+ // this is a binding, it is safe to remove
+ _ =>
+ Future.successful({})
+ } getOrElse {
+ // may only delete a package if all its ingredients are deleted already
+ WhiskAction
+ .listCollectionInNamespace(entityStore, wp.namespace.addPath(wp.name), skip = 0, limit = 0) flatMap {
+ case Left(list) if (list.size != 0) =>
+ Future failed {
+ RejectRequest(
+ Conflict,
+ s"Package not empty (contains ${list.size} ${if (list.size == 1) "entity" else "entities"})")
+ }
+ case _ => Future.successful({})
+ }
+ }
+ })
+ }
- /**
- * Gets package/binding.
- * The package/binding name is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskPackage has JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- getEntity(WhiskPackage, entityStore, entityName.toDocId, Some { mergePackageWithBinding() _ })
- }
+ /**
+ * Gets package/binding.
+ * The package/binding name is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskPackage has JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ getEntity(WhiskPackage, entityStore, entityName.toDocId, Some { mergePackageWithBinding() _ })
+ }
- /**
- * Gets all packages/bindings in namespace.
- *
- * Responses are one of (Code, Message)
- * - 200 [] or [WhiskPackage as JSON]
- * - 500 Internal Server Error
- */
- override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
- // for consistency, all the collections should support the same list API
- // but because supporting docs on actions is difficult, the API does not
- // offer an option to fetch entities with full docs yet; see comment in
- // Actions API for more.
- val docs = false
+ /**
+ * Gets all packages/bindings in namespace.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 [] or [WhiskPackage as JSON]
+ * - 500 Internal Server Error
+ */
+ override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
+ // for consistency, all the collections should support the same list API
+ // but because supporting docs on actions is difficult, the API does not
+ // offer an option to fetch entities with full docs yet; see comment in
+ // Actions API for more.
+ val docs = false
- // disable listing all public (shared) packages in all namespaces until
- // there exists a process in place to curate and rank these packages
- val publicPackagesInAnyNamespace = false
- parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) {
- (skip, limit, count) =>
- if (publicPackagesInAnyNamespace && docs) {
- terminate(BadRequest, "Parameters 'public' and 'docs' may not both be true at the same time")
- } else listEntities {
- if (!publicPackagesInAnyNamespace) {
- WhiskPackage.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map {
- list =>
- // any subject is entitled to list packages in any namespace
- // however, they shall only observe public packages if the packages
- // are not in one of the namespaces the subject is entitled to
- val packages = if (docs) {
- list.right.get map { WhiskPackage.serdes.write(_) }
- } else list.left.get
- FilterEntityList.filter(packages, excludePrivate,
- additionalFilter = { // additionally exclude bindings
- case pkg: JsObject => Try {
- pkg.fields(WhiskPackage.bindingFieldName) == JsBoolean(false)
- } getOrElse false
- })
- }
- } else {
- WhiskPackage.listCollectionInAnyNamespace(entityStore, skip, limit, docs = false, reduce = publicPackagesInAnyNamespace) map {
- _.left.get
- }
- }
- }
+ // disable listing all public (shared) packages in all namespaces until
+ // there exists a process in place to curate and rank these packages
+ val publicPackagesInAnyNamespace = false
+ parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) { (skip, limit, count) =>
+ if (publicPackagesInAnyNamespace && docs) {
+ terminate(BadRequest, "Parameters 'public' and 'docs' may not both be true at the same time")
+ } else
+ listEntities {
+ if (!publicPackagesInAnyNamespace) {
+ WhiskPackage.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map { list =>
+ // any subject is entitled to list packages in any namespace
+ // however, they shall only observe public packages if the packages
+ // are not in one of the namespaces the subject is entitled to
+ val packages = if (docs) {
+ list.right.get map { WhiskPackage.serdes.write(_) }
+ } else list.left.get
+ FilterEntityList.filter(packages, excludePrivate, additionalFilter = { // additionally exclude bindings
+ case pkg: JsObject =>
+ Try {
+ pkg.fields(WhiskPackage.bindingFieldName) == JsBoolean(false)
+ } getOrElse false
+ })
+ }
+ } else {
+ WhiskPackage.listCollectionInAnyNamespace(
+ entityStore,
+ skip,
+ limit,
+ docs = false,
+ reduce = publicPackagesInAnyNamespace) map {
+ _.left.get
+ }
+ }
}
}
+ }
- /**
- * Validates that a referenced binding exists.
- */
- private def checkBinding(binding: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[Unit] = {
- WhiskPackage.get(entityStore, binding.toDocId) recoverWith {
- case t: NoDocumentException => Future.failed(RejectRequest(BadRequest, Messages.bindingDoesNotExist))
- case t: DocumentTypeMismatchException => Future.failed(RejectRequest(Conflict, Messages.requestedBindingIsNotValid))
- case t => Future.failed(RejectRequest(BadRequest, t))
- } flatMap {
- // trying to create a new package binding that refers to another binding
- case provider if provider.binding.nonEmpty => Future.failed(RejectRequest(BadRequest, Messages.bindingCannotReferenceBinding))
- // or creating a package binding that refers to a package
- case _ => Future.successful({})
- }
+ /**
+ * Validates that a referenced binding exists.
+ */
+ private def checkBinding(binding: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[Unit] = {
+ WhiskPackage.get(entityStore, binding.toDocId) recoverWith {
+ case t: NoDocumentException => Future.failed(RejectRequest(BadRequest, Messages.bindingDoesNotExist))
+ case t: DocumentTypeMismatchException =>
+ Future.failed(RejectRequest(Conflict, Messages.requestedBindingIsNotValid))
+ case t => Future.failed(RejectRequest(BadRequest, t))
+ } flatMap {
+ // trying to create a new package binding that refers to another binding
+ case provider if provider.binding.nonEmpty =>
+ Future.failed(RejectRequest(BadRequest, Messages.bindingCannotReferenceBinding))
+ // or creating a package binding that refers to a package
+ case _ => Future.successful({})
}
+ }
- /**
- * Creates a WhiskPackage from PUT content, generating default values where necessary.
- * If this is a binding, confirm the referenced package exists.
- */
- private def create(content: WhiskPackagePut, pkgName: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[WhiskPackage] = {
- val validateBinding = content.binding map {
- b => checkBinding(b.fullyQualifiedName)
- } getOrElse Future.successful({})
+ /**
+ * Creates a WhiskPackage from PUT content, generating default values where necessary.
+ * If this is a binding, confirm the referenced package exists.
+ */
+ private def create(content: WhiskPackagePut, pkgName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Future[WhiskPackage] = {
+ val validateBinding = content.binding map { b =>
+ checkBinding(b.fullyQualifiedName)
+ } getOrElse Future.successful({})
- validateBinding map { _ =>
- WhiskPackage(
- pkgName.path,
- pkgName.name,
- content.binding,
- content.parameters getOrElse Parameters(),
- content.version getOrElse SemVer(),
- content.publish getOrElse false,
- // remove any binding annotation from PUT (always set by the controller)
- (content.annotations getOrElse Parameters())
- - WhiskPackage.bindingFieldName
- ++ bindingAnnotation(content.binding))
- }
+ validateBinding map { _ =>
+ WhiskPackage(
+ pkgName.path,
+ pkgName.name,
+ content.binding,
+ content.parameters getOrElse Parameters(),
+ content.version getOrElse SemVer(),
+ content.publish getOrElse false,
+ // remove any binding annotation from PUT (always set by the controller)
+ (content.annotations getOrElse Parameters())
+ - WhiskPackage.bindingFieldName
+ ++ bindingAnnotation(content.binding))
}
+ }
- /** Updates a WhiskPackage from PUT content, merging old package/binding where necessary. */
- private def update(content: WhiskPackagePut)(wp: WhiskPackage)(implicit transid: TransactionId): Future[WhiskPackage] = {
- val validateBinding = content.binding map { binding =>
- wp.binding map {
- // pre-existing entity is a binding, check that new binding is valid
- b => checkBinding(b.fullyQualifiedName)
- } getOrElse {
- // pre-existing entity is a package, cannot make it a binding
- Future.failed(RejectRequest(Conflict, Messages.packageCannotBecomeBinding))
- }
- } getOrElse Future.successful({})
+ /** Updates a WhiskPackage from PUT content, merging old package/binding where necessary. */
+ private def update(content: WhiskPackagePut)(wp: WhiskPackage)(
+ implicit transid: TransactionId): Future[WhiskPackage] = {
+ val validateBinding = content.binding map { binding =>
+ wp.binding map {
+ // pre-existing entity is a binding, check that new binding is valid
+ b =>
+ checkBinding(b.fullyQualifiedName)
+ } getOrElse {
+ // pre-existing entity is a package, cannot make it a binding
+ Future.failed(RejectRequest(Conflict, Messages.packageCannotBecomeBinding))
+ }
+ } getOrElse Future.successful({})
- validateBinding map { _ =>
- WhiskPackage(
- wp.namespace,
- wp.name,
- content.binding orElse wp.binding,
- content.parameters getOrElse wp.parameters,
- content.version getOrElse wp.version.upPatch,
- content.publish getOrElse wp.publish,
- // override any binding annotation from PUT (always set by the controller)
- (content.annotations getOrElse wp.annotations)
- - WhiskPackage.bindingFieldName
- ++ bindingAnnotation(content.binding orElse wp.binding)).
- revision[WhiskPackage](wp.docinfo.rev)
- }
+ validateBinding map { _ =>
+ WhiskPackage(
+ wp.namespace,
+ wp.name,
+ content.binding orElse wp.binding,
+ content.parameters getOrElse wp.parameters,
+ content.version getOrElse wp.version.upPatch,
+ content.publish getOrElse wp.publish,
+ // override any binding annotation from PUT (always set by the controller)
+ (content.annotations getOrElse wp.annotations)
+ - WhiskPackage.bindingFieldName
+ ++ bindingAnnotation(content.binding orElse wp.binding)).revision[WhiskPackage](wp.docinfo.rev)
}
+ }
- private def rewriteEntitlementFailure(failure: Throwable)(
- implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
- logging.info(this, s"rewriting failure $failure")
- failure match {
- case RejectRequest(NotFound, _) => terminate(BadRequest, Messages.bindingDoesNotExist)
- case RejectRequest(Conflict, _) => terminate(Conflict, Messages.requestedBindingIsNotValid)
- case _ => super.handleEntitlementFailure(failure)
- }
+ private def rewriteEntitlementFailure(failure: Throwable)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
+ logging.info(this, s"rewriting failure $failure")
+ failure match {
+ case RejectRequest(NotFound, _) => terminate(BadRequest, Messages.bindingDoesNotExist)
+ case RejectRequest(Conflict, _) => terminate(Conflict, Messages.requestedBindingIsNotValid)
+ case _ => super.handleEntitlementFailure(failure)
}
+ }
- /**
- * Constructs a "binding" annotation. This is redundant with the binding
- * information available in WhiskPackage but necessary for some clients which
- * fetch package lists but cannot determine which package may be bound. An
- * alternative is to include the binding in the package list "view" but this
- * will require an API change. So using an annotation instead.
- */
- private def bindingAnnotation(binding: Option[Binding]): Parameters = {
- binding map {
- b => Parameters(WhiskPackage.bindingFieldName, Binding.serdes.write(b))
- } getOrElse Parameters()
- }
+ /**
+ * Constructs a "binding" annotation. This is redundant with the binding
+ * information available in WhiskPackage but necessary for some clients which
+ * fetch package lists but cannot determine which package may be bound. An
+ * alternative is to include the binding in the package list "view" but this
+ * will require an API change. So using an annotation instead.
+ */
+ private def bindingAnnotation(binding: Option[Binding]): Parameters = {
+ binding map { b =>
+ Parameters(WhiskPackage.bindingFieldName, Binding.serdes.write(b))
+ } getOrElse Parameters()
+ }
- /**
- * Constructs a WhiskPackage that is a merger of a package with its packing binding (if any).
- * If this is a binding, fetch package for binding, merge parameters then emit.
- * Otherwise this is a package, emit it.
- */
- private def mergePackageWithBinding(ref: Option[WhiskPackage] = None)(wp: WhiskPackage)(implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
- wp.binding map {
- case b: Binding =>
- val docid = b.fullyQualifiedName.toDocId
- logging.info(this, s"fetching package '$docid' for reference")
- getEntity(WhiskPackage, entityStore, docid, Some {
- mergePackageWithBinding(Some { wp }) _
- })
- } getOrElse {
- val pkg = ref map { _ inherit wp.parameters } getOrElse wp
- logging.info(this, s"fetching package actions in '${wp.fullPath}'")
- val actions = WhiskAction.listCollectionInNamespace(entityStore, wp.fullPath, skip = 0, limit = 0) flatMap {
- case Left(list) => Future.successful {
- pkg withPackageActions (list map { o => WhiskPackageAction.serdes.read(o) })
- }
- case t => Future.failed {
- logging.error(this, "unexpected result in package action lookup: $t")
- new IllegalStateException(s"unexpected result in package action lookup: $t")
- }
- }
+ /**
+ * Constructs a WhiskPackage that is a merger of a package with its packing binding (if any).
+ * If this is a binding, fetch package for binding, merge parameters then emit.
+ * Otherwise this is a package, emit it.
+ */
+ private def mergePackageWithBinding(ref: Option[WhiskPackage] = None)(wp: WhiskPackage)(
+ implicit transid: TransactionId): RequestContext => Future[RouteResult] = {
+ wp.binding map {
+ case b: Binding =>
+ val docid = b.fullyQualifiedName.toDocId
+ logging.info(this, s"fetching package '$docid' for reference")
+ getEntity(WhiskPackage, entityStore, docid, Some {
+ mergePackageWithBinding(Some { wp }) _
+ })
+ } getOrElse {
+ val pkg = ref map { _ inherit wp.parameters } getOrElse wp
+ logging.info(this, s"fetching package actions in '${wp.fullPath}'")
+ val actions = WhiskAction.listCollectionInNamespace(entityStore, wp.fullPath, skip = 0, limit = 0) flatMap {
+ case Left(list) =>
+ Future.successful {
+ pkg withPackageActions (list map { o =>
+ WhiskPackageAction.serdes.read(o)
+ })
+ }
+ case t =>
+ Future.failed {
+ logging.error(this, "unexpected result in package action lookup: $t")
+ new IllegalStateException(s"unexpected result in package action lookup: $t")
+ }
+ }
- onComplete(actions) {
- case Success(p) =>
- logging.info(this, s"[GET] entity success")
- complete(OK, p)
- case Failure(t) =>
- logging.error(this, s"[GET] failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
- }
+ onComplete(actions) {
+ case Success(p) =>
+ logging.info(this, s"[GET] entity success")
+ complete(OK, p)
+ case Failure(t) =>
+ logging.error(this, s"[GET] failed: ${t.getMessage}")
+ terminate(InternalServerError)
+ }
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala b/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala
index 7fd3231..d5526b8 100644
--- a/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala
@@ -51,74 +51,75 @@ import whisk.core.loadBalancer.LoadBalancerService
protected[controller] class SwaggerDocs(apipath: Uri.Path, doc: String)(implicit actorSystem: ActorSystem)
extends Directives {
- /** Swagger end points. */
- protected val swaggeruipath = "docs"
- protected val swaggerdocpath = "api-docs"
+ /** Swagger end points. */
+ protected val swaggeruipath = "docs"
+ protected val swaggerdocpath = "api-docs"
- def basepath(url: Uri.Path = apipath): String = {
- (if (url.startsWithSlash) url else Uri.Path./(url)).toString
- }
+ def basepath(url: Uri.Path = apipath): String = {
+ (if (url.startsWithSlash) url else Uri.Path./(url)).toString
+ }
- /**
- * Defines the routes to serve the swagger docs.
- */
- val swaggerRoutes: Route = {
- pathPrefix(swaggeruipath) {
- getFromDirectory("/swagger-ui/")
- } ~ path(swaggeruipath) {
- redirect(s"$swaggeruipath/index.html?url=$apiDocsUrl", PermanentRedirect)
- } ~ pathPrefix(swaggerdocpath) {
- pathEndOrSingleSlash {
- getFromResource(doc)
- }
- }
+ /**
+ * Defines the routes to serve the swagger docs.
+ */
+ val swaggerRoutes: Route = {
+ pathPrefix(swaggeruipath) {
+ getFromDirectory("/swagger-ui/")
+ } ~ path(swaggeruipath) {
+ redirect(s"$swaggeruipath/index.html?url=$apiDocsUrl", PermanentRedirect)
+ } ~ pathPrefix(swaggerdocpath) {
+ pathEndOrSingleSlash {
+ getFromResource(doc)
+ }
}
+ }
- /** Forces add leading slash for swagger api-doc url rewrite to work. */
- private def apiDocsUrl = basepath(apipath / swaggerdocpath)
+ /** Forces add leading slash for swagger api-doc url rewrite to work. */
+ private def apiDocsUrl = basepath(apipath / swaggerdocpath)
}
protected[controller] object RestApiCommons {
- def requiredProperties = Map(WhiskConfig.servicePort -> 8080.toString) ++
- WhiskConfig.whiskVersion ++
- WhiskAuthStore.requiredProperties ++
- WhiskEntityStore.requiredProperties ++
- WhiskActivationStore.requiredProperties ++
- EntitlementProvider.requiredProperties ++
- WhiskActionsApi.requiredProperties ++
- Authenticate.requiredProperties ++
- Collection.requiredProperties
-
- import akka.http.scaladsl.model.HttpCharsets
- import akka.http.scaladsl.model.MediaTypes.`application/json`
- import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
- import akka.http.scaladsl.unmarshalling.Unmarshaller
+ def requiredProperties =
+ Map(WhiskConfig.servicePort -> 8080.toString) ++
+ WhiskConfig.whiskVersion ++
+ WhiskAuthStore.requiredProperties ++
+ WhiskEntityStore.requiredProperties ++
+ WhiskActivationStore.requiredProperties ++
+ EntitlementProvider.requiredProperties ++
+ WhiskActionsApi.requiredProperties ++
+ Authenticate.requiredProperties ++
+ Collection.requiredProperties
- /**
- * Extract an empty entity into a JSON object. This is useful for the
- * main APIs which accept JSON content type by default but may accept
- * no entity in the request.
- */
- implicit val emptyEntityToJsObject: FromEntityUnmarshaller[JsObject] = {
- Unmarshaller.byteStringUnmarshaller.forContentTypes(`application/json`).mapWithCharset { (data, charset) =>
- if (data.size == 0) {
- JsObject()
- } else {
- val input = {
- if (charset == HttpCharsets.`UTF-8`) ParserInput(data.toArray)
- else ParserInput(data.decodeString(charset.nioCharset))
- }
+ import akka.http.scaladsl.model.HttpCharsets
+ import akka.http.scaladsl.model.MediaTypes.`application/json`
+ import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
+ import akka.http.scaladsl.unmarshalling.Unmarshaller
- JsonParser(input).asJsObject
- }
+ /**
+ * Extract an empty entity into a JSON object. This is useful for the
+ * main APIs which accept JSON content type by default but may accept
+ * no entity in the request.
+ */
+ implicit val emptyEntityToJsObject: FromEntityUnmarshaller[JsObject] = {
+ Unmarshaller.byteStringUnmarshaller.forContentTypes(`application/json`).mapWithCharset { (data, charset) =>
+ if (data.size == 0) {
+ JsObject()
+ } else {
+ val input = {
+ if (charset == HttpCharsets.`UTF-8`) ParserInput(data.toArray)
+ else ParserInput(data.decodeString(charset.nioCharset))
}
+
+ JsonParser(input).asJsObject
+ }
}
+ }
- /** Pretty print JSON response. */
- implicit val jsonPrettyResponsePrinter = PrettyPrinter
+ /** Pretty print JSON response. */
+ implicit val jsonPrettyResponsePrinter = PrettyPrinter
- /** Standard compact JSON printer. */
- implicit val jsonDefaultResponsePrinter = CompactPrinter
+ /** Standard compact JSON printer. */
+ implicit val jsonDefaultResponsePrinter = CompactPrinter
}
/**
@@ -126,181 +127,172 @@ protected[controller] object RestApiCommons {
* Useful for CORS.
*/
protected[controller] trait RespondWithHeaders extends Directives {
- val allowOrigin = `Access-Control-Allow-Origin`.*
- val allowHeaders = `Access-Control-Allow-Headers`("Authorization", "Content-Type")
- val sendCorsHeaders = respondWithHeaders(allowOrigin, allowHeaders)
+ val allowOrigin = `Access-Control-Allow-Origin`.*
+ val allowHeaders = `Access-Control-Allow-Headers`("Authorization", "Content-Type")
+ val sendCorsHeaders = respondWithHeaders(allowOrigin, allowHeaders)
}
class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)(
- implicit val activeAckTopicIndex: InstanceId,
- implicit val actorSystem: ActorSystem,
- implicit val materializer: ActorMaterializer,
- implicit val logging: Logging,
- implicit val entityStore: EntityStore,
- implicit val entitlementProvider: EntitlementProvider,
- implicit val activationIdFactory: ActivationIdGenerator,
- implicit val loadBalancer: LoadBalancerService,
- implicit val cacheChangeNotification: Some[CacheChangeNotification],
- implicit val activationStore: ActivationStore,
- implicit val whiskConfig: WhiskConfig)
+ implicit val activeAckTopicIndex: InstanceId,
+ implicit val actorSystem: ActorSystem,
+ implicit val materializer: ActorMaterializer,
+ implicit val logging: Logging,
+ implicit val entityStore: EntityStore,
+ implicit val entitlementProvider: EntitlementProvider,
+ implicit val activationIdFactory: ActivationIdGenerator,
+ implicit val loadBalancer: LoadBalancerService,
+ implicit val cacheChangeNotification: Some[CacheChangeNotification],
+ implicit val activationStore: ActivationStore,
+ implicit val whiskConfig: WhiskConfig)
extends SwaggerDocs(Uri.Path(apiPath) / apiVersion, "apiv1swagger.json")
with Authenticate
with AuthenticatedRoute
with RespondWithHeaders {
- implicit val executionContext = actorSystem.dispatcher
- implicit val authStore = WhiskAuthStore.datastore(config)
+ implicit val executionContext = actorSystem.dispatcher
+ implicit val authStore = WhiskAuthStore.datastore(config)
- def prefix = pathPrefix(apiPath / apiVersion)
+ def prefix = pathPrefix(apiPath / apiVersion)
- /**
- * Describes details of a particular API path.
- */
- val info = (pathEndOrSingleSlash & get) {
- complete(JsObject(
- "description" -> "OpenWhisk API".toJson,
- "api_version" -> SemVer(1, 0, 0).toJson,
- "api_version_path" -> apiVersion.toJson,
- "build" -> whiskConfig(whiskVersionDate).toJson,
- "buildno" -> whiskConfig(whiskVersionBuildno).toJson,
- "swagger_paths" -> JsObject(
- "ui" -> s"/$swaggeruipath".toJson,
- "api-docs" -> s"/$swaggerdocpath".toJson)))
- }
+ /**
+ * Describes details of a particular API path.
+ */
+ val info = (pathEndOrSingleSlash & get) {
+ complete(
+ JsObject(
+ "description" -> "OpenWhisk API".toJson,
+ "api_version" -> SemVer(1, 0, 0).toJson,
+ "api_version_path" -> apiVersion.toJson,
+ "build" -> whiskConfig(whiskVersionDate).toJson,
+ "buildno" -> whiskConfig(whiskVersionBuildno).toJson,
+ "swagger_paths" -> JsObject("ui" -> s"/$swaggeruipath".toJson, "api-docs" -> s"/$swaggerdocpath".toJson)))
+ }
- def routes(implicit transid: TransactionId): Route = {
- prefix {
- sendCorsHeaders {
- info ~ basicAuth(validateCredentials) { user =>
- namespaces.routes(user) ~
- pathPrefix(Collection.NAMESPACES) {
- actions.routes(user) ~
- triggers.routes(user) ~
- rules.routes(user) ~
- activations.routes(user) ~
- packages.routes(user)
- }
- } ~ {
- swaggerRoutes
- }
- } ~ {
- // web actions are distinct to separate the cors header
- // and allow the actions themselves to respond to options
- basicAuth(validateCredentials) { user =>
- web.routes(user)
- } ~ {
- web.routes()
- } ~ options {
- sendCorsHeaders {
- complete(OK)
- }
- }
+ def routes(implicit transid: TransactionId): Route = {
+ prefix {
+ sendCorsHeaders {
+ info ~ basicAuth(validateCredentials) { user =>
+ namespaces.routes(user) ~
+ pathPrefix(Collection.NAMESPACES) {
+ actions.routes(user) ~
+ triggers.routes(user) ~
+ rules.routes(user) ~
+ activations.routes(user) ~
+ packages.routes(user)
}
+ } ~ {
+ swaggerRoutes
}
-
+ } ~ {
+ // web actions are distinct to separate the cors header
+ // and allow the actions themselves to respond to options
+ basicAuth(validateCredentials) { user =>
+ web.routes(user)
+ } ~ {
+ web.routes()
+ } ~ options {
+ sendCorsHeaders {
+ complete(OK)
+ }
+ }
+ }
}
- private val namespaces = new NamespacesApi(apiPath, apiVersion)
- private val actions = new ActionsApi(apiPath, apiVersion)
- private val packages = new PackagesApi(apiPath, apiVersion)
- private val triggers = new TriggersApi(apiPath, apiVersion)
- private val activations = new ActivationsApi(apiPath, apiVersion)
- private val rules = new RulesApi(apiPath, apiVersion)
- private val WebApiDirectives = new WebApiDirectives()
- private val web = new WebActionsApi(Seq("web"), this.WebApiDirectives)
+ }
- class NamespacesApi(
- val apiPath: String,
- val apiVersion: String)(
- implicit override val entityStore: EntityStore,
- override val entitlementProvider: EntitlementProvider,
- override val executionContext: ExecutionContext,
- override val logging: Logging)
- extends WhiskNamespacesApi
+ private val namespaces = new NamespacesApi(apiPath, apiVersion)
+ private val actions = new ActionsApi(apiPath, apiVersion)
+ private val packages = new PackagesApi(apiPath, apiVersion)
+ private val triggers = new TriggersApi(apiPath, apiVersion)
+ private val activations = new ActivationsApi(apiPath, apiVersion)
+ private val rules = new RulesApi(apiPath, apiVersion)
+ private val WebApiDirectives = new WebApiDirectives()
+ private val web = new WebActionsApi(Seq("web"), this.WebApiDirectives)
- class ActionsApi(
- val apiPath: String,
- val apiVersion: String)(
- implicit override val actorSystem: ActorSystem,
- override val activeAckTopicIndex: InstanceId,
- override val entityStore: EntityStore,
- override val activationStore: ActivationStore,
- override val entitlementProvider: EntitlementProvider,
- override val activationIdFactory: ActivationIdGenerator,
- override val loadBalancer: LoadBalancerService,
- override val cacheChangeNotification: Some[CacheChangeNotification],
- override val executionContext: ExecutionContext,
- override val logging: Logging,
- override val whiskConfig: WhiskConfig)
- extends WhiskActionsApi with WhiskServices {
- logging.info(this, s"actionSequenceLimit '${whiskConfig.actionSequenceLimit}'")(TransactionId.controller)
- assert(whiskConfig.actionSequenceLimit.toInt > 0)
- }
+ class NamespacesApi(val apiPath: String, val apiVersion: String)(
+ implicit override val entityStore: EntityStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val executionContext: ExecutionContext,
+ override val logging: Logging)
+ extends WhiskNamespacesApi
+
+ class ActionsApi(val apiPath: String, val apiVersion: String)(
+ implicit override val actorSystem: ActorSystem,
+ override val activeAckTopicIndex: InstanceId,
+ override val entityStore: EntityStore,
+ override val activationStore: ActivationStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val activationIdFactory: ActivationIdGenerator,
+ override val loadBalancer: LoadBalancerService,
+ override val cacheChangeNotification: Some[CacheChangeNotification],
+ override val executionContext: ExecutionContext,
+ override val logging: Logging,
+ override val whiskConfig: WhiskConfig)
+ extends WhiskActionsApi
+ with WhiskServices {
+ logging.info(this, s"actionSequenceLimit '${whiskConfig.actionSequenceLimit}'")(TransactionId.controller)
+ assert(whiskConfig.actionSequenceLimit.toInt > 0)
+ }
- class ActivationsApi(
- val apiPath: String,
- val apiVersion: String)(
- implicit override val activationStore: ActivationStore,
- override val entitlementProvider: EntitlementProvider,
- override val executionContext: ExecutionContext,
- override val logging: Logging)
- extends WhiskActivationsApi
+ class ActivationsApi(val apiPath: String, val apiVersion: String)(
+ implicit override val activationStore: ActivationStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val executionContext: ExecutionContext,
+ override val logging: Logging)
+ extends WhiskActivationsApi
- class PackagesApi(
- val apiPath: String,
- val apiVersion: String)(
- implicit override val entityStore: EntityStore,
- override val entitlementProvider: EntitlementProvider,
- override val activationIdFactory: ActivationIdGenerator,
- override val loadBalancer: LoadBalancerService,
- override val cacheChangeNotification: Some[CacheChangeNotification],
- override val executionContext: ExecutionContext,
- override val logging: Logging,
- override val whiskConfig: WhiskConfig)
- extends WhiskPackagesApi with WhiskServices
+ class PackagesApi(val apiPath: String, val apiVersion: String)(
+ implicit override val entityStore: EntityStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val activationIdFactory: ActivationIdGenerator,
+ override val loadBalancer: LoadBalancerService,
+ override val cacheChangeNotification: Some[CacheChangeNotification],
+ override val executionContext: ExecutionContext,
+ override val logging: Logging,
+ override val whiskConfig: WhiskConfig)
+ extends WhiskPackagesApi
+ with WhiskServices
- class RulesApi(
- val apiPath: String,
- val apiVersion: String)(
- implicit override val actorSystem: ActorSystem,
- override val entityStore: EntityStore,
- override val entitlementProvider: EntitlementProvider,
- override val activationIdFactory: ActivationIdGenerator,
- override val loadBalancer: LoadBalancerService,
- override val cacheChangeNotification: Some[CacheChangeNotification],
- override val executionContext: ExecutionContext,
- override val logging: Logging,
- override val whiskConfig: WhiskConfig)
- extends WhiskRulesApi with WhiskServices
+ class RulesApi(val apiPath: String, val apiVersion: String)(
+ implicit override val actorSystem: ActorSystem,
+ override val entityStore: EntityStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val activationIdFactory: ActivationIdGenerator,
+ override val loadBalancer: LoadBalancerService,
+ override val cacheChangeNotification: Some[CacheChangeNotification],
+ override val executionContext: ExecutionContext,
+ override val logging: Logging,
+ override val whiskConfig: WhiskConfig)
+ extends WhiskRulesApi
+ with WhiskServices
- class TriggersApi(
- val apiPath: String,
- val apiVersion: String)(
- implicit override val actorSystem: ActorSystem,
- implicit override val entityStore: EntityStore,
- override val entitlementProvider: EntitlementProvider,
- override val activationStore: ActivationStore,
- override val activationIdFactory: ActivationIdGenerator,
- override val loadBalancer: LoadBalancerService,
- override val cacheChangeNotification: Some[CacheChangeNotification],
- override val executionContext: ExecutionContext,
- override val logging: Logging,
- override val whiskConfig: WhiskConfig,
- override val materializer: ActorMaterializer)
- extends WhiskTriggersApi with WhiskServices
+ class TriggersApi(val apiPath: String, val apiVersion: String)(
+ implicit override val actorSystem: ActorSystem,
+ implicit override val entityStore: EntityStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val activationStore: ActivationStore,
+ override val activationIdFactory: ActivationIdGenerator,
+ override val loadBalancer: LoadBalancerService,
+ override val cacheChangeNotification: Some[CacheChangeNotification],
+ override val executionContext: ExecutionContext,
+ override val logging: Logging,
+ override val whiskConfig: WhiskConfig,
+ override val materializer: ActorMaterializer)
+ extends WhiskTriggersApi
+ with WhiskServices
- protected[controller] class WebActionsApi(
- override val webInvokePathSegments: Seq[String],
- override val webApiDirectives: WebApiDirectives)(
- implicit override val authStore: AuthStore,
- implicit val entityStore: EntityStore,
- override val activeAckTopicIndex: InstanceId,
- override val activationStore: ActivationStore,
- override val entitlementProvider: EntitlementProvider,
- override val activationIdFactory: ActivationIdGenerator,
- override val loadBalancer: LoadBalancerService,
- override val actorSystem: ActorSystem,
- override val executionContext: ExecutionContext,
- override val logging: Logging,
- override val whiskConfig: WhiskConfig)
- extends WhiskWebActionsApi with WhiskServices
+ protected[controller] class WebActionsApi(override val webInvokePathSegments: Seq[String],
+ override val webApiDirectives: WebApiDirectives)(
+ implicit override val authStore: AuthStore,
+ implicit val entityStore: EntityStore,
+ override val activeAckTopicIndex: InstanceId,
+ override val activationStore: ActivationStore,
+ override val entitlementProvider: EntitlementProvider,
+ override val activationIdFactory: ActivationIdGenerator,
+ override val loadBalancer: LoadBalancerService,
+ override val actorSystem: ActorSystem,
+ override val executionContext: ExecutionContext,
+ override val logging: Logging,
+ override val whiskConfig: WhiskConfig)
+ extends WhiskWebActionsApi
+ with WhiskServices
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/Rules.scala b/core/controller/src/main/scala/whisk/core/controller/Rules.scala
index 6f657b2..2a7bba0 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Rules.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Rules.scala
@@ -42,354 +42,376 @@ import whisk.core.entitlement.ReferencedEntities
/** A trait implementing the rules API */
trait WhiskRulesApi extends WhiskCollectionAPI with ReferencedEntities {
- services: WhiskServices =>
-
- protected override val collection = Collection(Collection.RULES)
-
- /** An actor system for timed based futures. */
- protected implicit val actorSystem: ActorSystem
-
- /** Database service to CRUD rules. */
- protected val entityStore: EntityStore
-
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
-
- /** Notification service for cache invalidation. */
- protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
-
- /** Path to Rules REST API. */
- protected val rulesPath = "rules"
-
- /**
- * Creates or updates rule if it already exists. The PUT content is deserialized into a WhiskRulePut
- * which is a subset of WhiskRule (it eschews the namespace, entity name and status since the former
- * are derived from the authenticated user and the URI and the status is managed automatically).
- * The WhiskRulePut is merged with the existing WhiskRule in the datastore, overriding old values
- * with new values that are defined. Any values not defined in the PUT content are replaced with
- * old values.
- *
- * The rule will not update if the status of the entity in the datastore is not INACTIVE. It rejects
- * such requests with Conflict.
- *
- * The create/update is also guarded by a predicate that confirm the trigger and action are valid.
- * Otherwise rejects the request with Bad Request and an appropriate message. It is true that the
- * trigger/action may be deleted after creation but at the very least confirming dependences here
- * prevents use errors where a rule is created with an invalid trigger/action which then fails
- * testing (fire a trigger and expect an action activation to occur).
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskRule as JSON
- * - 400 Bad Request
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- parameter('overwrite ? false) { overwrite =>
- entity(as[WhiskRulePut]) { content =>
- val request = content.resolve(entityName.namespace)
- onComplete(entitlementProvider.check(user, Privilege.READ, referencedEntities(request))) {
- case Success(_) =>
- putEntity(WhiskRule, entityStore, entityName.toDocId, overwrite,
- update(request) _, () => { create(request, entityName) },
- postProcess = Some { rule: WhiskRule =>
- completeAsRuleResponse(rule, Status.ACTIVE)
- })
- case Failure(f) =>
- handleEntitlementFailure(f)
- }
- }
- }
- }
-
- /**
- * Toggles rule status from enabled -> disabled and vice versa. The action are not confirmed
- * to still exist. This is deferred to trigger activation which will fail to post activations
- * for non-existent actions.
- *
- * Responses are one of (Code, Message)
- * - 200 OK rule in desired state
- * - 202 Accepted rule state change accepted
- * - 404 Not Found
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- extractStatusRequest { requestedState =>
- val docid = entityName.toDocId
-
- getEntity(WhiskRule, entityStore, docid, Some {
- rule: WhiskRule =>
- val ruleName = rule.fullyQualifiedName(false)
-
- val changeStatus = getTrigger(rule.trigger) map { trigger =>
- getStatus(trigger, ruleName)
- } flatMap { oldStatus =>
- if (requestedState != oldStatus) {
- logging.info(this, s"[POST] rule state change initiated: ${oldStatus} -> $requestedState")
- Future successful requestedState
- } else {
- logging.info(this, s"[POST] rule state will not be changed, the requested state is the same as the old state: ${oldStatus} -> $requestedState")
- Future failed { IgnoredRuleActivation(requestedState == oldStatus) }
- }
- } flatMap {
- case (newStatus) =>
- logging.info(this, s"[POST] attempting to set rule state to: ${newStatus}")
- WhiskTrigger.get(entityStore, rule.trigger.toDocId) flatMap { trigger =>
- val newTrigger = trigger.removeRule(ruleName)
- val triggerLink = ReducedRule(rule.action, newStatus)
- WhiskTrigger.put(entityStore, newTrigger.addRule(ruleName, triggerLink))
- }
- }
-
- onComplete(changeStatus) {
- case Success(response) =>
- complete(OK)
- case Failure(t) => t match {
- case _: DocumentConflictException =>
- logging.info(this, s"[POST] rule update conflict")
- terminate(Conflict, conflictMessage)
- case IgnoredRuleActivation(ok) =>
- logging.info(this, s"[POST] rule update ignored")
- if (ok) complete(OK) else terminate(Conflict)
- case _: NoDocumentException =>
- logging.info(this, s"[POST] the trigger attached to the rule doesn't exist")
- terminate(NotFound, "Only rules with existing triggers can be activated")
- case _: DeserializationException =>
- logging.error(this, s"[POST] rule update failed: ${t.getMessage}")
- terminate(InternalServerError, corruptedEntity)
- case _: Throwable =>
- logging.error(this, s"[POST] rule update failed: ${t.getMessage}")
- terminate(InternalServerError)
- }
- }
+ services: WhiskServices =>
+
+ protected override val collection = Collection(Collection.RULES)
+
+ /** An actor system for timed based futures. */
+ protected implicit val actorSystem: ActorSystem
+
+ /** Database service to CRUD rules. */
+ protected val entityStore: EntityStore
+
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
+
+ /** Notification service for cache invalidation. */
+ protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
+
+ /** Path to Rules REST API. */
+ protected val rulesPath = "rules"
+
+ /**
+ * Creates or updates rule if it already exists. The PUT content is deserialized into a WhiskRulePut
+ * which is a subset of WhiskRule (it eschews the namespace, entity name and status since the former
+ * are derived from the authenticated user and the URI and the status is managed automatically).
+ * The WhiskRulePut is merged with the existing WhiskRule in the datastore, overriding old values
+ * with new values that are defined. Any values not defined in the PUT content are replaced with
+ * old values.
+ *
+ * The rule will not update if the status of the entity in the datastore is not INACTIVE. It rejects
+ * such requests with Conflict.
+ *
+ * The create/update is also guarded by a predicate that confirm the trigger and action are valid.
+ * Otherwise rejects the request with Bad Request and an appropriate message. It is true that the
+ * trigger/action may be deleted after creation but at the very least confirming dependences here
+ * prevents use errors where a rule is created with an invalid trigger/action which then fails
+ * testing (fire a trigger and expect an action activation to occur).
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskRule as JSON
+ * - 400 Bad Request
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ parameter('overwrite ? false) { overwrite =>
+ entity(as[WhiskRulePut]) { content =>
+ val request = content.resolve(entityName.namespace)
+ onComplete(entitlementProvider.check(user, Privilege.READ, referencedEntities(request))) {
+ case Success(_) =>
+ putEntity(WhiskRule, entityStore, entityName.toDocId, overwrite, update(request) _, () => {
+ create(request, entityName)
+ }, postProcess = Some { rule: WhiskRule =>
+ completeAsRuleResponse(rule, Status.ACTIVE)
})
+ case Failure(f) =>
+ handleEntitlementFailure(f)
}
+ }
}
-
- /**
- * Deletes rule iff rule is inactive.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskRule as JSON
- * - 404 Not Found
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- deleteEntity(WhiskRule, entityStore, entityName.toDocId, (r: WhiskRule) => {
- val ruleName = FullyQualifiedEntityName(r.namespace, r.name)
- getTrigger(r.trigger) map { trigger =>
- (getStatus(trigger, ruleName), trigger)
- } flatMap {
- case (status, triggerOpt) =>
- triggerOpt map { trigger =>
- WhiskTrigger.put(entityStore, trigger.removeRule(ruleName)) map { _ => {} }
- } getOrElse Future.successful({})
- }
- }, postProcess = Some { rule: WhiskRule =>
- completeAsRuleResponse(rule, Status.INACTIVE)
- })
+ }
+
+ /**
+ * Toggles rule status from enabled -> disabled and vice versa. The action are not confirmed
+ * to still exist. This is deferred to trigger activation which will fail to post activations
+ * for non-existent actions.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 OK rule in desired state
+ * - 202 Accepted rule state change accepted
+ * - 404 Not Found
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ extractStatusRequest { requestedState =>
+ val docid = entityName.toDocId
+
+ getEntity(WhiskRule, entityStore, docid, Some {
+ rule: WhiskRule =>
+ val ruleName = rule.fullyQualifiedName(false)
+
+ val changeStatus = getTrigger(rule.trigger) map { trigger =>
+ getStatus(trigger, ruleName)
+ } flatMap {
+ oldStatus =>
+ if (requestedState != oldStatus) {
+ logging.info(this, s"[POST] rule state change initiated: ${oldStatus} -> $requestedState")
+ Future successful requestedState
+ } else {
+ logging.info(
+ this,
+ s"[POST] rule state will not be changed, the requested state is the same as the old state: ${oldStatus} -> $requestedState")
+ Future failed { IgnoredRuleActivation(requestedState == oldStatus) }
+ }
+ } flatMap {
+ case (newStatus) =>
+ logging.info(this, s"[POST] attempting to set rule state to: ${newStatus}")
+ WhiskTrigger.get(entityStore, rule.trigger.toDocId) flatMap { trigger =>
+ val newTrigger = trigger.removeRule(ruleName)
+ val triggerLink = ReducedRule(rule.action, newStatus)
+ WhiskTrigger.put(entityStore, newTrigger.addRule(ruleName, triggerLink))
+ }
+ }
+
+ onComplete(changeStatus) {
+ case Success(response) =>
+ complete(OK)
+ case Failure(t) =>
+ t match {
+ case _: DocumentConflictException =>
+ logging.info(this, s"[POST] rule update conflict")
+ terminate(Conflict, conflictMessage)
+ case IgnoredRuleActivation(ok) =>
+ logging.info(this, s"[POST] rule update ignored")
+ if (ok) complete(OK) else terminate(Conflict)
+ case _: NoDocumentException =>
+ logging.info(this, s"[POST] the trigger attached to the rule doesn't exist")
+ terminate(NotFound, "Only rules with existing triggers can be activated")
+ case _: DeserializationException =>
+ logging.error(this, s"[POST] rule update failed: ${t.getMessage}")
+ terminate(InternalServerError, corruptedEntity)
+ case _: Throwable =>
+ logging.error(this, s"[POST] rule update failed: ${t.getMessage}")
+ terminate(InternalServerError)
+ }
+ }
+ })
}
-
- /**
- * Gets rule. The rule name is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskRule has JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- getEntity(WhiskRule, entityStore, entityName.toDocId, Some { rule: WhiskRule =>
- val getRuleWithStatus = getTrigger(rule.trigger) map { trigger =>
- getStatus(trigger, entityName)
- } map { status =>
- rule.withStatus(status)
- }
-
- onComplete(getRuleWithStatus) {
- case Success(r) => complete(OK, r)
- case Failure(t) => terminate(InternalServerError)
- }
- })
- }
-
- /**
- * Gets all rules in namespace.
- *
- * Responses are one of (Code, Message)
- * - 200 [] or [WhiskRule as JSON]
- * - 500 Internal Server Error
- */
- override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
- // for consistency, all the collections should support the same list API
- // but because supporting docs on actions is difficult, the API does not
- // offer an option to fetch entities with full docs yet; see comment in
- // Actions API for more.
- val docs = false
- parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) {
- (skip, limit, count) =>
- listEntities {
- WhiskRule.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map {
- list =>
- val rules = if (docs) {
- list.right.get map { WhiskRule.serdes.write(_) }
- } else list.left.get
- FilterEntityList.filter(rules, excludePrivate)
- }
- }
+ }
+
+ /**
+ * Deletes rule iff rule is inactive.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskRule as JSON
+ * - 404 Not Found
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ deleteEntity(
+ WhiskRule,
+ entityStore,
+ entityName.toDocId,
+ (r: WhiskRule) => {
+ val ruleName = FullyQualifiedEntityName(r.namespace, r.name)
+ getTrigger(r.trigger) map { trigger =>
+ (getStatus(trigger, ruleName), trigger)
+ } flatMap {
+ case (status, triggerOpt) =>
+ triggerOpt map { trigger =>
+ WhiskTrigger.put(entityStore, trigger.removeRule(ruleName)) map { _ =>
+ {}
+ }
+ } getOrElse Future.successful({})
}
- }
-
- /** Creates a WhiskRule from PUT content, generating default values where necessary. */
- private def create(content: WhiskRulePut, ruleName: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[WhiskRule] = {
- if (content.trigger.isDefined && content.action.isDefined) {
- val triggerName = content.trigger.get
- val actionName = content.action.get
-
- checkTriggerAndActionExist(triggerName, actionName) recoverWith {
- case t => Future.failed(RejectRequest(BadRequest, t))
- } flatMap {
- case (trigger, action) =>
- val rule = WhiskRule(
- ruleName.path,
- ruleName.name,
- content.trigger.get,
- content.action.get,
- content.version getOrElse SemVer(),
- content.publish getOrElse false,
- content.annotations getOrElse Parameters())
-
- val triggerLink = ReducedRule(actionName, Status.ACTIVE)
- logging.info(this, s"about to put ${trigger.addRule(ruleName, triggerLink)}")
- WhiskTrigger.put(entityStore, trigger.addRule(ruleName, triggerLink)) map { _ => rule }
- }
- } else Future.failed(RejectRequest(BadRequest, "rule requires a valid trigger and a valid action"))
- }
-
- /** Updates a WhiskTrigger from PUT content, merging old trigger where necessary. */
- private def update(content: WhiskRulePut)(rule: WhiskRule)(implicit transid: TransactionId): Future[WhiskRule] = {
- val ruleName = FullyQualifiedEntityName(rule.namespace, rule.name)
- val oldTriggerName = rule.trigger
-
- getTrigger(oldTriggerName) flatMap { oldTriggerOpt =>
- val newTriggerEntity = content.trigger getOrElse rule.trigger
- val newTriggerName = newTriggerEntity
-
- val actionEntity = content.action getOrElse rule.action
- val actionName = actionEntity
-
- checkTriggerAndActionExist(newTriggerName, actionName) recoverWith {
- case t => Future.failed(RejectRequest(BadRequest, t))
- } flatMap {
- case (newTrigger, newAction) =>
- val r = WhiskRule(
- rule.namespace,
- rule.name,
- newTriggerEntity,
- actionEntity,
- content.version getOrElse rule.version.upPatch,
- content.publish getOrElse rule.publish,
- content.annotations getOrElse rule.annotations).
- revision[WhiskRule](rule.docinfo.rev)
-
- // Deletes reference from the old trigger iff it is different from the new one
- val deleteOldLink = for {
- isDifferentTrigger <- content.trigger.filter(_ => newTriggerName != oldTriggerName)
- oldTrigger <- oldTriggerOpt
- } yield {
- WhiskTrigger.put(entityStore, oldTrigger.removeRule(ruleName))
- }
-
- val triggerLink = ReducedRule(actionName, Status.INACTIVE)
- val update = WhiskTrigger.put(entityStore, newTrigger.addRule(ruleName, triggerLink))
- Future.sequence(Seq(deleteOldLink.getOrElse(Future.successful(true)), update)).map(_ => r)
- }
+ },
+ postProcess = Some { rule: WhiskRule =>
+ completeAsRuleResponse(rule, Status.INACTIVE)
+ })
+ }
+
+ /**
+ * Gets rule. The rule name is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskRule has JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ getEntity(
+ WhiskRule,
+ entityStore,
+ entityName.toDocId,
+ Some { rule: WhiskRule =>
+ val getRuleWithStatus = getTrigger(rule.trigger) map { trigger =>
+ getStatus(trigger, entityName)
+ } map { status =>
+ rule.withStatus(status)
}
- }
- /**
- * Gets a WhiskTrigger defined by the given DocInfo. Gracefully falls back to None iff the trigger is not found.
- *
- * @param tid DocInfo defining the trigger to get
- * @return a WhiskTrigger iff found, else None
- */
- private def getTrigger(t: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[Option[WhiskTrigger]] = {
- WhiskTrigger.get(entityStore, t.toDocId) map {
- trigger => Some(trigger)
- } recover {
- case _: NoDocumentException | DeserializationException(_, _, _) => None
+ onComplete(getRuleWithStatus) {
+ case Success(r) => complete(OK, r)
+ case Failure(t) => terminate(InternalServerError)
}
- }
-
- /**
- * Extracts the Status for the rule out of a WhiskTrigger that may be there. Falls back to INACTIVE if the trigger
- * could not be found or the rule being worked on has not yet been written into the trigger record.
- *
- * @param triggerOpt Option containing a WhiskTrigger
- * @param ruleName Namespace the name of the rule being worked on
- * @return Status of the rule
- */
- private def getStatus(triggerOpt: Option[WhiskTrigger], ruleName: FullyQualifiedEntityName)(implicit transid: TransactionId): Status = {
- val statusFromTrigger = for {
- trigger <- triggerOpt
- rules <- trigger.rules
- rule <- rules.get(ruleName)
- } yield {
- rule.status
+ })
+ }
+
+ /**
+ * Gets all rules in namespace.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 [] or [WhiskRule as JSON]
+ * - 500 Internal Server Error
+ */
+ override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
+ // for consistency, all the collections should support the same list API
+ // but because supporting docs on actions is difficult, the API does not
+ // offer an option to fetch entities with full docs yet; see comment in
+ // Actions API for more.
+ val docs = false
+ parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) { (skip, limit, count) =>
+ listEntities {
+ WhiskRule.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map { list =>
+ val rules = if (docs) {
+ list.right.get map { WhiskRule.serdes.write(_) }
+ } else list.left.get
+ FilterEntityList.filter(rules, excludePrivate)
}
- statusFromTrigger getOrElse Status.INACTIVE
+ }
}
-
- /**
- * Completes an HTTP request with a WhiskRule including the computed Status
- *
- * @param rule the rule to send
- * @param status the status to include in the response
- */
- private def completeAsRuleResponse(rule: WhiskRule, status: Status = Status.INACTIVE): StandardRoute = {
- complete(OK, rule.withStatus(status))
+ }
+
+ /** Creates a WhiskRule from PUT content, generating default values where necessary. */
+ private def create(content: WhiskRulePut, ruleName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Future[WhiskRule] = {
+ if (content.trigger.isDefined && content.action.isDefined) {
+ val triggerName = content.trigger.get
+ val actionName = content.action.get
+
+ checkTriggerAndActionExist(triggerName, actionName) recoverWith {
+ case t => Future.failed(RejectRequest(BadRequest, t))
+ } flatMap {
+ case (trigger, action) =>
+ val rule = WhiskRule(
+ ruleName.path,
+ ruleName.name,
+ content.trigger.get,
+ content.action.get,
+ content.version getOrElse SemVer(),
+ content.publish getOrElse false,
+ content.annotations getOrElse Parameters())
+
+ val triggerLink = ReducedRule(actionName, Status.ACTIVE)
+ logging.info(this, s"about to put ${trigger.addRule(ruleName, triggerLink)}")
+ WhiskTrigger.put(entityStore, trigger.addRule(ruleName, triggerLink)) map { _ =>
+ rule
+ }
+ }
+ } else Future.failed(RejectRequest(BadRequest, "rule requires a valid trigger and a valid action"))
+ }
+
+ /** Updates a WhiskTrigger from PUT content, merging old trigger where necessary. */
+ private def update(content: WhiskRulePut)(rule: WhiskRule)(implicit transid: TransactionId): Future[WhiskRule] = {
+ val ruleName = FullyQualifiedEntityName(rule.namespace, rule.name)
+ val oldTriggerName = rule.trigger
+
+ getTrigger(oldTriggerName) flatMap { oldTriggerOpt =>
+ val newTriggerEntity = content.trigger getOrElse rule.trigger
+ val newTriggerName = newTriggerEntity
+
+ val actionEntity = content.action getOrElse rule.action
+ val actionName = actionEntity
+
+ checkTriggerAndActionExist(newTriggerName, actionName) recoverWith {
+ case t => Future.failed(RejectRequest(BadRequest, t))
+ } flatMap {
+ case (newTrigger, newAction) =>
+ val r = WhiskRule(
+ rule.namespace,
+ rule.name,
+ newTriggerEntity,
+ actionEntity,
+ content.version getOrElse rule.version.upPatch,
+ content.publish getOrElse rule.publish,
+ content.annotations getOrElse rule.annotations).revision[WhiskRule](rule.docinfo.rev)
+
+ // Deletes reference from the old trigger iff it is different from the new one
+ val deleteOldLink = for {
+ isDifferentTrigger <- content.trigger.filter(_ => newTriggerName != oldTriggerName)
+ oldTrigger <- oldTriggerOpt
+ } yield {
+ WhiskTrigger.put(entityStore, oldTrigger.removeRule(ruleName))
+ }
+
+ val triggerLink = ReducedRule(actionName, Status.INACTIVE)
+ val update = WhiskTrigger.put(entityStore, newTrigger.addRule(ruleName, triggerLink))
+ Future.sequence(Seq(deleteOldLink.getOrElse(Future.successful(true)), update)).map(_ => r)
+ }
}
-
- /**
- * Checks if trigger and action are valid documents (that is, they exist) in the datastore.
- *
- * @param trigger the trigger id
- * @param action the action id
- * @return future that completes with references trigger and action if they exist
- */
- private def checkTriggerAndActionExist(trigger: FullyQualifiedEntityName, action: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[(WhiskTrigger, WhiskAction)] = {
-
- for {
- triggerExists <- WhiskTrigger.get(entityStore, trigger.toDocId) recoverWith {
- case _: NoDocumentException => Future.failed {
- new NoDocumentException(s"trigger ${trigger.qualifiedNameWithLeadingSlash} does not exist")
- }
- case _: DeserializationException => Future.failed {
- new DeserializationException(s"trigger ${trigger.qualifiedNameWithLeadingSlash} is corrupted")
- }
- }
-
- actionExists <- WhiskAction.resolveAction(entityStore, action) flatMap {
- resolvedName => WhiskAction.get(entityStore, resolvedName.toDocId)
- } recoverWith {
- case _: NoDocumentException => Future.failed {
- new NoDocumentException(s"action ${action.qualifiedNameWithLeadingSlash} does not exist")
- }
- case _: DeserializationException => Future.failed {
- new DeserializationException(s"action ${action.qualifiedNameWithLeadingSlash} is corrupted")
- }
- }
- } yield (triggerExists, actionExists)
+ }
+
+ /**
+ * Gets a WhiskTrigger defined by the given DocInfo. Gracefully falls back to None iff the trigger is not found.
+ *
+ * @param tid DocInfo defining the trigger to get
+ * @return a WhiskTrigger iff found, else None
+ */
+ private def getTrigger(t: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[Option[WhiskTrigger]] = {
+ WhiskTrigger.get(entityStore, t.toDocId) map { trigger =>
+ Some(trigger)
+ } recover {
+ case _: NoDocumentException | DeserializationException(_, _, _) => None
}
-
- /** Extracts status request subject to allowed values. */
- private def extractStatusRequest = {
- implicit val statusSerdes = Status.serdesRestricted
- entity(as[Status])
+ }
+
+ /**
+ * Extracts the Status for the rule out of a WhiskTrigger that may be there. Falls back to INACTIVE if the trigger
+ * could not be found or the rule being worked on has not yet been written into the trigger record.
+ *
+ * @param triggerOpt Option containing a WhiskTrigger
+ * @param ruleName Namespace the name of the rule being worked on
+ * @return Status of the rule
+ */
+ private def getStatus(triggerOpt: Option[WhiskTrigger], ruleName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Status = {
+ val statusFromTrigger = for {
+ trigger <- triggerOpt
+ rules <- trigger.rules
+ rule <- rules.get(ruleName)
+ } yield {
+ rule.status
}
+ statusFromTrigger getOrElse Status.INACTIVE
+ }
+
+ /**
+ * Completes an HTTP request with a WhiskRule including the computed Status
+ *
+ * @param rule the rule to send
+ * @param status the status to include in the response
+ */
+ private def completeAsRuleResponse(rule: WhiskRule, status: Status = Status.INACTIVE): StandardRoute = {
+ complete(OK, rule.withStatus(status))
+ }
+
+ /**
+ * Checks if trigger and action are valid documents (that is, they exist) in the datastore.
+ *
+ * @param trigger the trigger id
+ * @param action the action id
+ * @return future that completes with references trigger and action if they exist
+ */
+ private def checkTriggerAndActionExist(trigger: FullyQualifiedEntityName, action: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Future[(WhiskTrigger, WhiskAction)] = {
+
+ for {
+ triggerExists <- WhiskTrigger.get(entityStore, trigger.toDocId) recoverWith {
+ case _: NoDocumentException =>
+ Future.failed {
+ new NoDocumentException(s"trigger ${trigger.qualifiedNameWithLeadingSlash} does not exist")
+ }
+ case _: DeserializationException =>
+ Future.failed {
+ new DeserializationException(s"trigger ${trigger.qualifiedNameWithLeadingSlash} is corrupted")
+ }
+ }
+
+ actionExists <- WhiskAction.resolveAction(entityStore, action) flatMap { resolvedName =>
+ WhiskAction.get(entityStore, resolvedName.toDocId)
+ } recoverWith {
+ case _: NoDocumentException =>
+ Future.failed {
+ new NoDocumentException(s"action ${action.qualifiedNameWithLeadingSlash} does not exist")
+ }
+ case _: DeserializationException =>
+ Future.failed {
+ new DeserializationException(s"action ${action.qualifiedNameWithLeadingSlash} is corrupted")
+ }
+ }
+ } yield (triggerExists, actionExists)
+ }
+
+ /** Extracts status request subject to allowed values. */
+ private def extractStatusRequest = {
+ implicit val statusSerdes = Status.serdesRestricted
+ entity(as[Status])
+ }
}
private case class IgnoredRuleActivation(noop: Boolean) extends Throwable
diff --git a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
index c9d1444..7f191b0 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
@@ -21,7 +21,7 @@ import java.time.Clock
import java.time.Instant
import scala.concurrent.Future
-import scala.util.{ Failure, Success }
+import scala.util.{Failure, Success}
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
@@ -64,279 +64,288 @@ import whisk.http.ErrorResponse.terminate
/** A trait implementing the triggers API. */
trait WhiskTriggersApi extends WhiskCollectionAPI {
- services: WhiskServices =>
-
- protected override val collection = Collection(Collection.TRIGGERS)
-
- /** An actor system for timed based futures. */
- protected implicit val actorSystem: ActorSystem
-
- /** Database service to CRUD triggers. */
- protected val entityStore: EntityStore
-
- /** Notification service for cache invalidation. */
- protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
-
- /** Database service to get activations. */
- protected val activationStore: ActivationStore
-
- /** JSON response formatter. */
- import RestApiCommons.jsonDefaultResponsePrinter
-
- /** Path to Triggers REST API. */
- protected val triggersPath = "triggers"
-
- protected implicit val materializer: ActorMaterializer
-
- import RestApiCommons.emptyEntityToJsObject
-
- /**
- * Creates or updates trigger if it already exists. The PUT content is deserialized into a WhiskTriggerPut
- * which is a subset of WhiskTrigger (it eschews the namespace and entity name since the former is derived
- * from the authenticated user and the latter is derived from the URI). The WhiskTriggerPut is merged with
- * the existing WhiskTrigger in the datastore, overriding old values with new values that are defined.
- * Any values not defined in the PUT content are replaced with old values.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskAction as JSON
- * - 400 Bad Request
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- parameter('overwrite ? false) { overwrite =>
- entity(as[WhiskTriggerPut]) { content =>
- putEntity(WhiskTrigger, entityStore, entityName.toDocId, overwrite,
- update(content) _,
- () => { create(content, entityName) },
- postProcess = Some { trigger =>
- completeAsTriggerResponse(trigger)
- })
- }
- }
+ services: WhiskServices =>
+
+ protected override val collection = Collection(Collection.TRIGGERS)
+
+ /** An actor system for timed based futures. */
+ protected implicit val actorSystem: ActorSystem
+
+ /** Database service to CRUD triggers. */
+ protected val entityStore: EntityStore
+
+ /** Notification service for cache invalidation. */
+ protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
+
+ /** Database service to get activations. */
+ protected val activationStore: ActivationStore
+
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
+
+ /** Path to Triggers REST API. */
+ protected val triggersPath = "triggers"
+
+ protected implicit val materializer: ActorMaterializer
+
+ import RestApiCommons.emptyEntityToJsObject
+
+ /**
+ * Creates or updates trigger if it already exists. The PUT content is deserialized into a WhiskTriggerPut
+ * which is a subset of WhiskTrigger (it eschews the namespace and entity name since the former is derived
+ * from the authenticated user and the latter is derived from the URI). The WhiskTriggerPut is merged with
+ * the existing WhiskTrigger in the datastore, overriding old values with new values that are defined.
+ * Any values not defined in the PUT content are replaced with old values.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskAction as JSON
+ * - 400 Bad Request
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ parameter('overwrite ? false) { overwrite =>
+ entity(as[WhiskTriggerPut]) { content =>
+ putEntity(WhiskTrigger, entityStore, entityName.toDocId, overwrite, update(content) _, () => {
+ create(content, entityName)
+ }, postProcess = Some { trigger =>
+ completeAsTriggerResponse(trigger)
+ })
+ }
}
-
- /**
- * Fires trigger if it exists. The POST content is deserialized into a Payload and posted
- * to the loadbalancer.
- *
- * Responses are one of (Code, Message)
- * - 200 ActivationId as JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- entity(as[Option[JsObject]]) {
- payload =>
- getEntity(WhiskTrigger, entityStore, entityName.toDocId, Some {
- trigger: WhiskTrigger =>
- val args = trigger.parameters.merge(payload)
- val triggerActivationId = activationIdFactory.make()
- logging.info(this, s"[POST] trigger activation id: ${triggerActivationId}")
-
- val triggerActivation = WhiskActivation(
- namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
- entityName.name,
- user.subject,
- triggerActivationId,
- Instant.now(Clock.systemUTC()),
- Instant.EPOCH,
- response = ActivationResponse.success(payload orElse Some(JsObject())),
- version = trigger.version,
- duration = None)
- logging.info(this, s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
- val saveTriggerActivation = WhiskActivation.put(activationStore, triggerActivation) map {
- _ => triggerActivationId
+ }
+
+ /**
+ * Fires trigger if it exists. The POST content is deserialized into a Payload and posted
+ * to the loadbalancer.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 ActivationId as JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ entity(as[Option[JsObject]]) { payload =>
+ getEntity(WhiskTrigger, entityStore, entityName.toDocId, Some {
+ trigger: WhiskTrigger =>
+ val args = trigger.parameters.merge(payload)
+ val triggerActivationId = activationIdFactory.make()
+ logging.info(this, s"[POST] trigger activation id: ${triggerActivationId}")
+
+ val triggerActivation = WhiskActivation(
+ namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
+ entityName.name,
+ user.subject,
+ triggerActivationId,
+ Instant.now(Clock.systemUTC()),
+ Instant.EPOCH,
+ response = ActivationResponse.success(payload orElse Some(JsObject())),
+ version = trigger.version,
+ duration = None)
+ logging.info(this, s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
+ val saveTriggerActivation = WhiskActivation.put(activationStore, triggerActivation) map { _ =>
+ triggerActivationId
+ }
+
+ val url = Uri(s"http://localhost:${whiskConfig.servicePort}")
+
+ trigger.rules.map {
+ _.filter {
+ case (ruleName, rule) => rule.status == Status.ACTIVE
+ } foreach {
+ case (ruleName, rule) =>
+ val ruleActivation = WhiskActivation(
+ namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
+ ruleName.name,
+ user.subject,
+ activationIdFactory.make(),
+ Instant.now(Clock.systemUTC()),
+ Instant.EPOCH,
+ cause = Some(triggerActivationId),
+ response = ActivationResponse.success(),
+ version = trigger.version,
+ duration = None)
+ logging.info(this, s"[POST] rule ${ruleName} activated, writing activation record to datastore")
+ WhiskActivation.put(activationStore, ruleActivation)
+
+ val actionNamespace = rule.action.path.root.asString
+ val actionPath = {
+ rule.action.path.relativePath.map { pkg =>
+ (Path.SingleSlash + pkg.namespace) / rule.action.name.asString
+ } getOrElse {
+ Path.SingleSlash + rule.action.name.asString
+ }
+ }.toString
+
+ val actionUrl = Path("/api/v1") / "namespaces" / actionNamespace / "actions"
+ val request = HttpRequest(
+ method = POST,
+ uri = url.withPath(actionUrl + actionPath),
+ headers =
+ List(Authorization(BasicHttpCredentials(user.authkey.uuid.asString, user.authkey.key.asString))),
+ entity = HttpEntity(MediaTypes.`application/json`, args.getOrElse(JsObject()).compactPrint))
+
+ Http().singleRequest(request).map {
+ response =>
+ response.status match {
+ case OK | Accepted =>
+ Unmarshal(response.entity).to[JsObject].map { a =>
+ logging.info(this, s"${rule.action} activated ${a.fields("activationId")}")
}
-
- val url = Uri(s"http://localhost:${whiskConfig.servicePort}")
-
- trigger.rules.map {
- _.filter {
- case (ruleName, rule) => rule.status == Status.ACTIVE
- } foreach {
- case (ruleName, rule) =>
- val ruleActivation = WhiskActivation(
- namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
- ruleName.name,
- user.subject,
- activationIdFactory.make(),
- Instant.now(Clock.systemUTC()),
- Instant.EPOCH,
- cause = Some(triggerActivationId),
- response = ActivationResponse.success(),
- version = trigger.version,
- duration = None)
- logging.info(this, s"[POST] rule ${ruleName} activated, writing activation record to datastore")
- WhiskActivation.put(activationStore, ruleActivation)
-
- val actionNamespace = rule.action.path.root.asString
- val actionPath = {
- rule.action.path.relativePath.map {
- pkg => (Path.SingleSlash + pkg.namespace) / rule.action.name.asString
- } getOrElse {
- Path.SingleSlash + rule.action.name.asString
- }
- }.toString
-
- val actionUrl = Path("/api/v1") / "namespaces" / actionNamespace / "actions"
- val request = HttpRequest(
- method = POST,
- uri = url.withPath(actionUrl + actionPath),
- headers = List(Authorization(BasicHttpCredentials(user.authkey.uuid.asString, user.authkey.key.asString))),
- entity = HttpEntity(MediaTypes.`application/json`, args.getOrElse(JsObject()).compactPrint))
-
- Http().singleRequest(request).map { response =>
- response.status match {
- case OK | Accepted => Unmarshal(response.entity).to[JsObject].map { a =>
- logging.info(this, s"${rule.action} activated ${a.fields("activationId")}")
- }
- case NotFound =>
- response.discardEntityBytes()
- logging.info(this, s"${rule.action} failed, action not found")
- case _ => Unmarshal(response.entity).to[String].map { error =>
- logging.warn(this, s"${rule.action} failed due to $error")
- }
- }
- }
- }
- }
-
- onComplete(saveTriggerActivation) {
- case Success(activationId) =>
- complete(OK, activationId.toJsObject)
- case Failure(t: Throwable) =>
- logging.error(this, s"[POST] storing trigger activation failed: ${t.getMessage}")
- terminate(InternalServerError)
+ case NotFound =>
+ response.discardEntityBytes()
+ logging.info(this, s"${rule.action} failed, action not found")
+ case _ =>
+ Unmarshal(response.entity).to[String].map { error =>
+ logging.warn(this, s"${rule.action} failed due to $error")
}
- })
- }
- }
-
- /**
- * Deletes trigger.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskTrigger as JSON
- * - 404 Not Found
- * - 409 Conflict
- * - 500 Internal Server Error
- */
- override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
- deleteEntity(WhiskTrigger, entityStore, entityName.toDocId, (t: WhiskTrigger) => Future.successful({}), postProcess = Some { trigger =>
- completeAsTriggerResponse(trigger)
- })
- }
-
- /**
- * Gets trigger. The trigger name is prefixed with the namespace to create the primary index key.
- *
- * Responses are one of (Code, Message)
- * - 200 WhiskTrigger has JSON
- * - 404 Not Found
- * - 500 Internal Server Error
- */
- override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(implicit transid: TransactionId) = {
- getEntity(WhiskTrigger, entityStore, entityName.toDocId, Some { trigger =>
- completeAsTriggerResponse(trigger)
- })
- }
-
- /**
- * Gets all triggers in namespace.
- *
- * Responses are one of (Code, Message)
- * - 200 [] or [WhiskTrigger as JSON]
- * - 500 Internal Server Error
- */
- override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
- // for consistency, all the collections should support the same list API
- // but because supporting docs on actions is difficult, the API does not
- // offer an option to fetch entities with full docs yet; see comment in
- // Actions API for more.
- val docs = false
- parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) {
- (skip, limit, count) =>
- listEntities {
- WhiskTrigger.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map {
- list =>
- val triggers = if (docs) {
- list.right.get map { WhiskTrigger.serdes.write(_) }
- } else list.left.get
- FilterEntityList.filter(triggers, excludePrivate)
}
}
- }
- }
-
- /** Creates a WhiskTrigger from PUT content, generating default values where necessary. */
- private def create(content: WhiskTriggerPut, triggerName: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[WhiskTrigger] = {
- val newTrigger = WhiskTrigger(
- triggerName.path,
- triggerName.name,
- content.parameters getOrElse Parameters(),
- content.limits getOrElse TriggerLimits(),
- content.version getOrElse SemVer(),
- content.publish getOrElse false,
- content.annotations getOrElse Parameters())
- validateTriggerFeed(newTrigger)
- }
-
- /** Updates a WhiskTrigger from PUT content, merging old trigger where necessary. */
- private def update(content: WhiskTriggerPut)(trigger: WhiskTrigger)(implicit transid: TransactionId): Future[WhiskTrigger] = {
- val newTrigger = WhiskTrigger(
- trigger.namespace,
- trigger.name,
- content.parameters getOrElse trigger.parameters,
- content.limits getOrElse trigger.limits,
- content.version getOrElse trigger.version.upPatch,
- content.publish getOrElse trigger.publish,
- content.annotations getOrElse trigger.annotations,
- trigger.rules).
- revision[WhiskTrigger](trigger.docinfo.rev)
-
- // feed must be specified in create, and cannot be added as a trigger update
- content.annotations flatMap { _.get(Parameters.Feed) } map { _ =>
- Future failed {
- RejectRequest(BadRequest, "A trigger feed is only permitted when the trigger is created")
}
- } getOrElse {
- Future successful newTrigger
- }
+ }
+
+ onComplete(saveTriggerActivation) {
+ case Success(activationId) =>
+ complete(OK, activationId.toJsObject)
+ case Failure(t: Throwable) =>
+ logging.error(this, s"[POST] storing trigger activation failed: ${t.getMessage}")
+ terminate(InternalServerError)
+ }
+ })
}
-
- /**
- * Validates a trigger feed annotation.
- * A trigger feed must be a valid entity name, e.g., one of 'namespace/package/name'
- * or 'namespace/name', or just 'name'.
- *
- * TODO: check if the feed actually exists. This is deferred because the macro
- * operation of creating a trigger and initializing the feed is handled as one
- * atomic operation in the CLI and the UI. At some point these may be promoted
- * to a single atomic operation in the controller; at which point, validating
- * the trigger feed should execute the action (verifies it is a valid name that
- * the subject is entitled to) and iff that succeeds will the trigger be created
- * or updated.
- */
- private def validateTriggerFeed(trigger: WhiskTrigger)(implicit transid: TransactionId) = {
- trigger.annotations.get(Parameters.Feed) map {
- case JsString(f) if (EntityPath.validate(f)) =>
- Future successful trigger
- case _ => Future failed {
- RejectRequest(BadRequest, "Feed name is not valid")
- }
- } getOrElse {
- Future successful trigger
+ }
+
+ /**
+ * Deletes trigger.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskTrigger as JSON
+ * - 404 Not Found
+ * - 409 Conflict
+ * - 500 Internal Server Error
+ */
+ override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
+ deleteEntity(
+ WhiskTrigger,
+ entityStore,
+ entityName.toDocId,
+ (t: WhiskTrigger) => Future.successful({}),
+ postProcess = Some { trigger =>
+ completeAsTriggerResponse(trigger)
+ })
+ }
+
+ /**
+ * Gets trigger. The trigger name is prefixed with the namespace to create the primary index key.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 WhiskTrigger has JSON
+ * - 404 Not Found
+ * - 500 Internal Server Error
+ */
+ override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
+ implicit transid: TransactionId) = {
+ getEntity(WhiskTrigger, entityStore, entityName.toDocId, Some { trigger =>
+ completeAsTriggerResponse(trigger)
+ })
+ }
+
+ /**
+ * Gets all triggers in namespace.
+ *
+ * Responses are one of (Code, Message)
+ * - 200 [] or [WhiskTrigger as JSON]
+ * - 500 Internal Server Error
+ */
+ override def list(user: Identity, namespace: EntityPath, excludePrivate: Boolean)(implicit transid: TransactionId) = {
+ // for consistency, all the collections should support the same list API
+ // but because supporting docs on actions is difficult, the API does not
+ // offer an option to fetch entities with full docs yet; see comment in
+ // Actions API for more.
+ val docs = false
+ parameter('skip ? 0, 'limit ? collection.listLimit, 'count ? false) { (skip, limit, count) =>
+ listEntities {
+ WhiskTrigger.listCollectionInNamespace(entityStore, namespace, skip, limit, docs) map { list =>
+ val triggers = if (docs) {
+ list.right.get map { WhiskTrigger.serdes.write(_) }
+ } else list.left.get
+ FilterEntityList.filter(triggers, excludePrivate)
}
+ }
}
-
- /**
- * Completes an HTTP request with a WhiskRule including the computed Status
- *
- * @param rule the rule to send
- * @param status the status to include in the response
- */
- private def completeAsTriggerResponse(trigger: WhiskTrigger): RequestContext => Future[RouteResult] = {
- complete(OK, trigger.withoutRules)
+ }
+
+ /** Creates a WhiskTrigger from PUT content, generating default values where necessary. */
+ private def create(content: WhiskTriggerPut, triggerName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Future[WhiskTrigger] = {
+ val newTrigger = WhiskTrigger(
+ triggerName.path,
+ triggerName.name,
+ content.parameters getOrElse Parameters(),
+ content.limits getOrElse TriggerLimits(),
+ content.version getOrElse SemVer(),
+ content.publish getOrElse false,
+ content.annotations getOrElse Parameters())
+ validateTriggerFeed(newTrigger)
+ }
+
+ /** Updates a WhiskTrigger from PUT content, merging old trigger where necessary. */
+ private def update(content: WhiskTriggerPut)(trigger: WhiskTrigger)(
+ implicit transid: TransactionId): Future[WhiskTrigger] = {
+ val newTrigger = WhiskTrigger(
+ trigger.namespace,
+ trigger.name,
+ content.parameters getOrElse trigger.parameters,
+ content.limits getOrElse trigger.limits,
+ content.version getOrElse trigger.version.upPatch,
+ content.publish getOrElse trigger.publish,
+ content.annotations getOrElse trigger.annotations,
+ trigger.rules).revision[WhiskTrigger](trigger.docinfo.rev)
+
+ // feed must be specified in create, and cannot be added as a trigger update
+ content.annotations flatMap { _.get(Parameters.Feed) } map { _ =>
+ Future failed {
+ RejectRequest(BadRequest, "A trigger feed is only permitted when the trigger is created")
+ }
+ } getOrElse {
+ Future successful newTrigger
+ }
+ }
+
+ /**
+ * Validates a trigger feed annotation.
+ * A trigger feed must be a valid entity name, e.g., one of 'namespace/package/name'
+ * or 'namespace/name', or just 'name'.
+ *
+ * TODO: check if the feed actually exists. This is deferred because the macro
+ * operation of creating a trigger and initializing the feed is handled as one
+ * atomic operation in the CLI and the UI. At some point these may be promoted
+ * to a single atomic operation in the controller; at which point, validating
+ * the trigger feed should execute the action (verifies it is a valid name that
+ * the subject is entitled to) and iff that succeeds will the trigger be created
+ * or updated.
+ */
+ private def validateTriggerFeed(trigger: WhiskTrigger)(implicit transid: TransactionId) = {
+ trigger.annotations.get(Parameters.Feed) map {
+ case JsString(f) if (EntityPath.validate(f)) =>
+ Future successful trigger
+ case _ =>
+ Future failed {
+ RejectRequest(BadRequest, "Feed name is not valid")
+ }
+ } getOrElse {
+ Future successful trigger
}
+ }
+
+ /**
+ * Completes an HTTP request with a WhiskRule including the computed Status
+ *
+ * @param rule the rule to send
+ * @param status the status to include in the response
+ */
+ private def completeAsTriggerResponse(trigger: WhiskTrigger): RequestContext => Future[RouteResult] = {
+ complete(OK, trigger.withoutRules)
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/WebActions.scala b/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
index ea704ab..f893c54 100644
--- a/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
@@ -20,7 +20,7 @@ package whisk.core.controller
import java.util.Base64
import scala.concurrent.Future
-import scala.util.{ Failure, Success, Try }
+import scala.util.{Failure, Success, Try}
import akka.http.scaladsl.model.HttpEntity.Empty
import akka.http.scaladsl.server.Directives
@@ -41,14 +41,14 @@ import akka.http.scaladsl.model.headers.`Content-Type`
import akka.http.scaladsl.model.ContentType
import akka.http.scaladsl.model.ContentTypes
import akka.http.scaladsl.model.FormData
-import akka.http.scaladsl.model.HttpMethods.{ OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH }
+import akka.http.scaladsl.model.HttpMethods.{DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT}
import akka.http.scaladsl.model.HttpCharsets
import spray.json._
import spray.json.DefaultJsonProtocol._
import WhiskWebActionsApi.MediaExtension
-import RestApiCommons.{ jsonPrettyResponsePrinter => jsonPrettyPrinter }
+import RestApiCommons.{jsonPrettyResponsePrinter => jsonPrettyPrinter}
import whisk.common.TransactionId
import whisk.core.controller.actions.PostActionActivation
@@ -60,647 +60,650 @@ import whisk.http.Messages
import whisk.utils.JsHelpers._
protected[controller] sealed class WebApiDirectives(prefix: String = "__ow_") {
- // enforce the presence of an extension (e.g., .http) in the URI path
- val enforceExtension = false
-
- // the field name that represents the status code for an http action response
- val statusCode = "statusCode"
-
- // parameters that are added to an action input to pass HTTP request context values
- val method: String = fields("method")
- val headers: String = fields("headers")
- val path: String = fields("path")
- val namespace: String = fields("user")
- val query: String = fields("query")
- val body: String = fields("body")
-
- lazy val reservedProperties: Set[String] = Set(method, headers, path, namespace, query, body)
- protected final def fields(f: String) = s"$prefix$f"
+ // enforce the presence of an extension (e.g., .http) in the URI path
+ val enforceExtension = false
+
+ // the field name that represents the status code for an http action response
+ val statusCode = "statusCode"
+
+ // parameters that are added to an action input to pass HTTP request context values
+ val method: String = fields("method")
+ val headers: String = fields("headers")
+ val path: String = fields("path")
+ val namespace: String = fields("user")
+ val query: String = fields("query")
+ val body: String = fields("body")
+
+ lazy val reservedProperties: Set[String] = Set(method, headers, path, namespace, query, body)
+ protected final def fields(f: String) = s"$prefix$f"
}
-private case class Context(
- propertyMap: WebApiDirectives,
- method: HttpMethod,
- headers: Seq[HttpHeader],
- path: String,
- query: Query,
- body: Option[JsValue] = None) {
-
- val queryAsMap = query.toMap
-
- // returns true iff the attached query and body parameters contain a property
- // that conflicts with the given reserved parameters
- def overrides(reservedParams: Set[String]): Set[String] = {
- val queryParams = queryAsMap.keySet
- val bodyParams = body.map {
- case JsObject(fields) => fields.keySet
- case _ => Set.empty
- }.getOrElse(Set.empty)
-
- (queryParams ++ bodyParams) intersect reservedParams
+private case class Context(propertyMap: WebApiDirectives,
+ method: HttpMethod,
+ headers: Seq[HttpHeader],
+ path: String,
+ query: Query,
+ body: Option[JsValue] = None) {
+
+ val queryAsMap = query.toMap
+
+ // returns true iff the attached query and body parameters contain a property
+ // that conflicts with the given reserved parameters
+ def overrides(reservedParams: Set[String]): Set[String] = {
+ val queryParams = queryAsMap.keySet
+ val bodyParams = body
+ .map {
+ case JsObject(fields) => fields.keySet
+ case _ => Set.empty
+ }
+ .getOrElse(Set.empty)
+
+ (queryParams ++ bodyParams) intersect reservedParams
+ }
+
+ // attach the body to the Context
+ def withBody(b: Option[JsValue]) = Context(propertyMap, method, headers, path, query, b)
+
+ def metadata(user: Option[Identity]): Map[String, JsValue] = {
+ Map(
+ propertyMap.method -> method.value.toLowerCase.toJson,
+ propertyMap.headers -> headers.map(h => h.lowercaseName -> h.value).toMap.toJson,
+ propertyMap.path -> path.toJson) ++
+ user.map(u => propertyMap.namespace -> u.namespace.asString.toJson)
+ }
+
+ def toActionArgument(user: Option[Identity], boxQueryAndBody: Boolean): Map[String, JsValue] = {
+ val queryParams = if (boxQueryAndBody) {
+ Map(propertyMap.query -> JsString(query.toString))
+ } else {
+ queryAsMap.map(kv => kv._1 -> JsString(kv._2))
}
- // attach the body to the Context
- def withBody(b: Option[JsValue]) = Context(propertyMap, method, headers, path, query, b)
-
- def metadata(user: Option[Identity]): Map[String, JsValue] = {
- Map(propertyMap.method -> method.value.toLowerCase.toJson,
- propertyMap.headers -> headers.map(h => h.lowercaseName -> h.value).toMap.toJson,
- propertyMap.path -> path.toJson) ++
- user.map(u => propertyMap.namespace -> u.namespace.asString.toJson)
+ // if the body is a json object, merge with query parameters
+ // otherwise, this is an opaque body that will be nested under
+ // __ow_body in the parameters sent to the action as an argument
+ val bodyParams = body match {
+ case Some(JsObject(fields)) if !boxQueryAndBody => fields
+ case Some(v) => Map(propertyMap.body -> v)
+ case None if !boxQueryAndBody => Map.empty
+ case _ => Map(propertyMap.body -> JsObject())
}
- def toActionArgument(user: Option[Identity], boxQueryAndBody: Boolean): Map[String, JsValue] = {
- val queryParams = if (boxQueryAndBody) {
- Map(propertyMap.query -> JsString(query.toString))
- } else {
- queryAsMap.map(kv => kv._1 -> JsString(kv._2))
- }
-
- // if the body is a json object, merge with query parameters
- // otherwise, this is an opaque body that will be nested under
- // __ow_body in the parameters sent to the action as an argument
- val bodyParams = body match {
- case Some(JsObject(fields)) if !boxQueryAndBody => fields
- case Some(v) => Map(propertyMap.body -> v)
- case None if !boxQueryAndBody => Map.empty
- case _ => Map(propertyMap.body -> JsObject())
- }
-
- // precedence order is: query params -> body (last wins)
- metadata(user) ++ queryParams ++ bodyParams
- }
+ // precedence order is: query params -> body (last wins)
+ metadata(user) ++ queryParams ++ bodyParams
+ }
}
protected[core] object WhiskWebActionsApi extends Directives {
- private val mediaTranscoders = {
- // extensions are expected to contain only [a-z]
- Seq(MediaExtension(".http", None, false, resultAsHttp _),
- MediaExtension(".json", None, true, resultAsJson _),
- MediaExtension(".html", Some(List("html")), true, resultAsHtml _),
- MediaExtension(".svg", Some(List("svg")), true, resultAsSvg _),
- MediaExtension(".text", Some(List("text")), true, resultAsText _))
- }
-
- private val defaultMediaTranscoder: MediaExtension = mediaTranscoders.find(_.extension == ".http").get
-
- val allowedExtensions: Set[String] = mediaTranscoders.map(_.extension).toSet
-
- /**
- * Splits string into a base name plus optional extension.
- * If name ends with ".xxxx" which matches a known extension, accept it as the extension.
- * Otherwise, the extension is ".http" by definition unless enforcing the presence of an extension.
- */
- def mediaTranscoderForName(name: String, enforceExtension: Boolean): (String, Option[MediaExtension]) = {
- mediaTranscoders.find(mt => name.endsWith(mt.extension)).map { mt =>
- val base = name.dropRight(mt.extensionLength)
- (base, Some(mt))
- }.getOrElse {
- (name, if (enforceExtension) None else Some(defaultMediaTranscoder))
- }
- }
-
- /**
- * Supported extensions, their default projection and transcoder to complete a request.
- *
- * @param extension the supported media types for action response
- * @param defaultProject the default media extensions for action projection
- * @param transcoder the HTTP decoder and terminator for the extension
- */
- protected case class MediaExtension(
- extension: String,
- defaultProjection: Option[List[String]],
- projectionAllowed: Boolean,
- transcoder: (JsValue, TransactionId, WebApiDirectives) => Route) {
- val extensionLength = extension.length
+ private val mediaTranscoders = {
+ // extensions are expected to contain only [a-z]
+ Seq(
+ MediaExtension(".http", None, false, resultAsHttp _),
+ MediaExtension(".json", None, true, resultAsJson _),
+ MediaExtension(".html", Some(List("html")), true, resultAsHtml _),
+ MediaExtension(".svg", Some(List("svg")), true, resultAsSvg _),
+ MediaExtension(".text", Some(List("text")), true, resultAsText _))
+ }
+
+ private val defaultMediaTranscoder: MediaExtension = mediaTranscoders.find(_.extension == ".http").get
+
+ val allowedExtensions: Set[String] = mediaTranscoders.map(_.extension).toSet
+
+ /**
+ * Splits string into a base name plus optional extension.
+ * If name ends with ".xxxx" which matches a known extension, accept it as the extension.
+ * Otherwise, the extension is ".http" by definition unless enforcing the presence of an extension.
+ */
+ def mediaTranscoderForName(name: String, enforceExtension: Boolean): (String, Option[MediaExtension]) = {
+ mediaTranscoders
+ .find(mt => name.endsWith(mt.extension))
+ .map { mt =>
+ val base = name.dropRight(mt.extensionLength)
+ (base, Some(mt))
+ }
+ .getOrElse {
+ (name, if (enforceExtension) None else Some(defaultMediaTranscoder))
+ }
+ }
+
+ /**
+ * Supported extensions, their default projection and transcoder to complete a request.
+ *
+ * @param extension the supported media types for action response
+ * @param defaultProject the default media extensions for action projection
+ * @param transcoder the HTTP decoder and terminator for the extension
+ */
+ protected case class MediaExtension(extension: String,
+ defaultProjection: Option[List[String]],
+ projectionAllowed: Boolean,
+ transcoder: (JsValue, TransactionId, WebApiDirectives) => Route) {
+ val extensionLength = extension.length
+ }
+
+ private def resultAsHtml(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = result match {
+ case JsString(html) => complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, html))
+ case _ => terminate(BadRequest, Messages.invalidMedia(`text/html`))(transid, jsonPrettyPrinter)
+ }
+
+ private def resultAsSvg(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = result match {
+ case JsString(svg) => complete(HttpEntity(`image/svg+xml`, svg.getBytes))
+ case _ => terminate(BadRequest, Messages.invalidMedia(`image/svg+xml`))(transid, jsonPrettyPrinter)
+ }
+
+ private def resultAsText(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {
+ result match {
+ case r: JsObject => complete(OK, r.prettyPrint)
+ case r: JsArray => complete(OK, r.prettyPrint)
+ case JsString(s) => complete(OK, s)
+ case JsBoolean(b) => complete(OK, b.toString)
+ case JsNumber(n) => complete(OK, n.toString)
+ case JsNull => complete(OK, JsNull.toString)
}
+ }
- private def resultAsHtml(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = result match {
- case JsString(html) => complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, html))
- case _ => terminate(BadRequest, Messages.invalidMedia(`text/html`))(transid, jsonPrettyPrinter)
+ private def resultAsJson(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {
+ result match {
+ case r: JsObject => complete(OK, r)
+ case r: JsArray => complete(OK, r)
+ case _ => terminate(BadRequest, Messages.invalidMedia(`application/json`))(transid, jsonPrettyPrinter)
}
-
- private def resultAsSvg(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = result match {
- case JsString(svg) => complete(HttpEntity(`image/svg+xml`, svg.getBytes))
- case _ => terminate(BadRequest, Messages.invalidMedia(`image/svg+xml`))(transid, jsonPrettyPrinter)
- }
-
- private def resultAsText(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {
- result match {
- case r: JsObject => complete(OK, r.prettyPrint)
- case r: JsArray => complete(OK, r.prettyPrint)
- case JsString(s) => complete(OK, s)
- case JsBoolean(b) => complete(OK, b.toString)
- case JsNumber(n) => complete(OK, n.toString)
- case JsNull => complete(OK, JsNull.toString)
- }
- }
-
- private def resultAsJson(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {
- result match {
- case r: JsObject => complete(OK, r)
- case r: JsArray => complete(OK, r)
- case _ => terminate(BadRequest, Messages.invalidMedia(`application/json`))(transid, jsonPrettyPrinter)
+ }
+
+ private def resultAsHttp(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {
+ Try {
+ val JsObject(fields) = result
+ val headers = fields.get("headers").map {
+ case JsObject(hs) =>
+ hs.flatMap {
+ case (k, v) => headersFromJson(k, v)
+ }.toList
+
+ case _ => throw new Throwable("Invalid header")
+ } getOrElse List()
+
+ val code = fields.get(rp.statusCode).map {
+ case JsNumber(c) =>
+ // the following throws an exception if the code is
+ // not a whole number or a valid code
+ StatusCode.int2StatusCode(c.toIntExact)
+
+ case _ => throw new Throwable("Illegal code")
+ } getOrElse (OK)
+
+ fields.get("body") map {
+ case JsString(str) => interpretHttpResponse(code, headers, str, transid)
+ case js => interpretHttpResponseAsJson(code, headers, js, transid)
+ } getOrElse {
+ respondWithHeaders(removeContentTypeHeader(headers)) {
+ // note that if header defined a content-type, it will be ignored
+ // since the type must be compatible with the data response
+ complete(code, HttpEntity.Empty)
}
+ }
+ } getOrElse {
+ // either the result was not a JsObject or there was an exception validting the
+ // response as an http result
+ terminate(BadRequest, Messages.invalidMedia(`message/http`))(transid, jsonPrettyPrinter)
}
-
- private def resultAsHttp(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {
- Try {
- val JsObject(fields) = result
- val headers = fields.get("headers").map {
- case JsObject(hs) => hs.flatMap {
- case (k, v) => headersFromJson(k, v)
- }.toList
-
- case _ => throw new Throwable("Invalid header")
- } getOrElse List()
-
- val code = fields.get(rp.statusCode).map {
- case JsNumber(c) =>
- // the following throws an exception if the code is
- // not a whole number or a valid code
- StatusCode.int2StatusCode(c.toIntExact)
-
- case _ => throw new Throwable("Illegal code")
- } getOrElse (OK)
-
- fields.get("body") map {
- case JsString(str) => interpretHttpResponse(code, headers, str, transid)
- case js => interpretHttpResponseAsJson(code, headers, js, transid)
- } getOrElse {
- respondWithHeaders(removeContentTypeHeader(headers)) {
- // note that if header defined a content-type, it will be ignored
- // since the type must be compatible with the data response
- complete(code, HttpEntity.Empty)
- }
+ }
+
+ private def headersFromJson(k: String, v: JsValue): Seq[RawHeader] = v match {
+ case JsString(v) => Seq(RawHeader(k, v))
+ case JsBoolean(v) => Seq(RawHeader(k, v.toString))
+ case JsNumber(v) => Seq(RawHeader(k, v.toString))
+ case JsArray(v) => v.flatMap(inner => headersFromJson(k, inner))
+ case _ => throw new Throwable("Invalid header")
+ }
+
+ /**
+ * Finds the content-type in the header list and maps it to a known media type. If it is not
+ * recognized, construct a failure with appropriate message.
+ */
+ private def findContentTypeInHeader(headers: List[RawHeader],
+ transid: TransactionId,
+ defaultType: MediaType): Try[MediaType] = {
+ headers.find(_.lowercaseName == `Content-Type`.lowercaseName) match {
+ case Some(header) =>
+ MediaType.parse(header.value) match {
+ case Right(mediaType: MediaType) =>
+ // lookup the media type specified in the content header to see if it is a recognized type
+ MediaTypes.getForKey(mediaType.mainType -> mediaType.subType).map(Success(_)).getOrElse {
+ // this is a content-type that is not recognized, reject it
+ Failure(RejectRequest(BadRequest, Messages.httpUnknownContentType)(transid))
}
- } getOrElse {
- // either the result was not a JsObject or there was an exception validting the
- // response as an http result
- terminate(BadRequest, Messages.invalidMedia(`message/http`))(transid, jsonPrettyPrinter)
+ case _ => Failure(RejectRequest(BadRequest, Messages.httpUnknownContentType)(transid))
}
+ case None => Success(defaultType)
}
+ }
+
+ private def interpretHttpResponseAsJson(code: StatusCode,
+ headers: List[RawHeader],
+ js: JsValue,
+ transid: TransactionId) = {
+ findContentTypeInHeader(headers, transid, `application/json`) match {
+ case Success(mediaType) if (mediaType == `application/json`) =>
+ respondWithHeaders(removeContentTypeHeader(headers)) {
+ complete(code, js)
+ }
- private def headersFromJson(k: String, v: JsValue): Seq[RawHeader] = v match {
- case JsString(v) => Seq(RawHeader(k, v))
- case JsBoolean(v) => Seq(RawHeader(k, v.toString))
- case JsNumber(v) => Seq(RawHeader(k, v.toString))
- case JsArray(v) => v.flatMap(inner => headersFromJson(k, inner))
- case _ => throw new Throwable("Invalid header")
+ case _ =>
+ terminate(BadRequest, Messages.httpContentTypeError)(transid, jsonPrettyPrinter)
}
-
- /**
- * Finds the content-type in the header list and maps it to a known media type. If it is not
- * recognized, construct a failure with appropriate message.
- */
- private def findContentTypeInHeader(headers: List[RawHeader], transid: TransactionId, defaultType: MediaType): Try[MediaType] = {
- headers.find(_.lowercaseName == `Content-Type`.lowercaseName) match {
- case Some(header) =>
- MediaType.parse(header.value) match {
- case Right(mediaType: MediaType) =>
- // lookup the media type specified in the content header to see if it is a recognized type
- MediaTypes.getForKey(mediaType.mainType -> mediaType.subType).map(Success(_)).getOrElse {
- // this is a content-type that is not recognized, reject it
- Failure(RejectRequest(BadRequest, Messages.httpUnknownContentType)(transid))
- }
- case _ => Failure(RejectRequest(BadRequest, Messages.httpUnknownContentType)(transid))
- }
- case None => Success(defaultType)
+ }
+
+ private def interpretHttpResponse(code: StatusCode, headers: List[RawHeader], str: String, transid: TransactionId) = {
+ findContentTypeInHeader(headers, transid, `text/html`).flatMap { mediaType =>
+ val ct = ContentType(mediaType, () => HttpCharsets.`UTF-8`)
+ ct match {
+ case _: ContentType.Binary | ContentType(`application/json`, _) =>
+ // base64 encoded json response supported for legacy reasons
+ Try(Base64.getDecoder().decode(str)).map(HttpEntity(ct, _))
+
+ case nonbinary: ContentType.NonBinary => Success(HttpEntity(nonbinary, str))
+ }
+ } match {
+ case Success(entity) =>
+ respondWithHeaders(removeContentTypeHeader(headers)) {
+ complete(code, entity)
}
- }
- private def interpretHttpResponseAsJson(code: StatusCode, headers: List[RawHeader], js: JsValue, transid: TransactionId) = {
- findContentTypeInHeader(headers, transid, `application/json`) match {
- case Success(mediaType) if (mediaType == `application/json`) =>
- respondWithHeaders(removeContentTypeHeader(headers)) {
- complete(code, js)
- }
+ case Failure(RejectRequest(code, message)) =>
+ terminate(code, message)(transid, jsonPrettyPrinter)
- case _ =>
- terminate(BadRequest, Messages.httpContentTypeError)(transid, jsonPrettyPrinter)
- }
+ case _ =>
+ terminate(BadRequest, Messages.httpContentTypeError)(transid, jsonPrettyPrinter)
}
+ }
- private def interpretHttpResponse(code: StatusCode, headers: List[RawHeader], str: String, transid: TransactionId) = {
- findContentTypeInHeader(headers, transid, `text/html`).flatMap { mediaType =>
- val ct = ContentType(mediaType, () => HttpCharsets.`UTF-8`)
- ct match {
- case _: ContentType.Binary | ContentType(`application/json`, _) =>
- // base64 encoded json response supported for legacy reasons
- Try(Base64.getDecoder().decode(str)).map(HttpEntity(ct, _))
-
- case nonbinary: ContentType.NonBinary => Success(HttpEntity(nonbinary, str))
- }
- } match {
- case Success(entity) =>
- respondWithHeaders(removeContentTypeHeader(headers)) {
- complete(code, entity)
- }
+ private def removeContentTypeHeader(headers: List[RawHeader]) =
+ headers.filter(_.lowercaseName != `Content-Type`.lowercaseName)
+}
- case Failure(RejectRequest(code, message)) =>
- terminate(code, message)(transid, jsonPrettyPrinter)
+trait WhiskWebActionsApi extends Directives with ValidateRequestSize with PostActionActivation {
+ services: WhiskServices =>
- case _ =>
- terminate(BadRequest, Messages.httpContentTypeError)(transid, jsonPrettyPrinter)
- }
- }
+ /** API path invocation path for posting activations directly through the host. */
+ protected val webInvokePathSegments: Seq[String]
- private def removeContentTypeHeader(headers: List[RawHeader]) =
- headers.filter(_.lowercaseName != `Content-Type`.lowercaseName)
-}
+ /** Mapping of HTTP request fields to action parameter names. */
+ protected val webApiDirectives: WebApiDirectives
-trait WhiskWebActionsApi
- extends Directives
- with ValidateRequestSize
- with PostActionActivation {
- services: WhiskServices =>
+ /** Store for identities. */
+ protected val authStore: AuthStore
- /** API path invocation path for posting activations directly through the host. */
- protected val webInvokePathSegments: Seq[String]
+ /** The prefix for web invokes e.g., /web. */
+ private lazy val webRoutePrefix = {
+ pathPrefix(webInvokePathSegments.map(_segmentStringToPathMatcher(_)).reduceLeft(_ / _))
+ }
- /** Mapping of HTTP request fields to action parameter names. */
- protected val webApiDirectives: WebApiDirectives
+ /** Allowed verbs. */
+ private lazy val allowedOperations = get | delete | post | put | head | options | patch
- /** Store for identities. */
- protected val authStore: AuthStore
+ private lazy val validNameSegment = pathPrefix(EntityName.REGEX.r)
+ private lazy val packagePrefix = pathPrefix("default".r | EntityName.REGEX.r)
- /** The prefix for web invokes e.g., /web. */
- private lazy val webRoutePrefix = {
- pathPrefix(webInvokePathSegments.map(_segmentStringToPathMatcher(_)).reduceLeft(_ / _))
- }
+ private val defaultCorsResponse = List(
+ `Access-Control-Allow-Origin`.*,
+ `Access-Control-Allow-Methods`(OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH),
+ `Access-Control-Allow-Headers`(`Authorization`.name, `Content-Type`.name))
- /** Allowed verbs. */
- private lazy val allowedOperations = get | delete | post | put | head | options | patch
-
- private lazy val validNameSegment = pathPrefix(EntityName.REGEX.r)
- private lazy val packagePrefix = pathPrefix("default".r | EntityName.REGEX.r)
-
- private val defaultCorsResponse = List(
- `Access-Control-Allow-Origin`.*,
- `Access-Control-Allow-Methods`(OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH),
- `Access-Control-Allow-Headers`(`Authorization`.name, `Content-Type`.name))
-
- /** Extracts the HTTP method, headers, query params and unmatched (remaining) path. */
- private val requestMethodParamsAndPath = {
- extract { ctx =>
- val method = ctx.request.method
- val query = ctx.request.uri.query()
- val path = ctx.unmatchedPath.toString
- val headers = ctx.request.headers
- Context(webApiDirectives, method, headers, path, query)
- }
+ /** Extracts the HTTP method, headers, query params and unmatched (remaining) path. */
+ private val requestMethodParamsAndPath = {
+ extract { ctx =>
+ val method = ctx.request.method
+ val query = ctx.request.uri.query()
+ val path = ctx.unmatchedPath.toString
+ val headers = ctx.request.headers
+ Context(webApiDirectives, method, headers, path, query)
}
-
- def routes(user: Identity)(implicit transid: TransactionId): Route = routes(Some(user))
- def routes()(implicit transid: TransactionId): Route = routes(None)
-
- private val maxWaitForWebActionResult = Some(WhiskActionsApi.maxWaitForBlockingActivation)
-
- /**
- * Adds route to web based activations. Actions invoked this way are anonymous in that the
- * caller is not authenticated. The intended action must be named in the path as a fully qualified
- * name as in /web/some-namespace/some-package/some-action. The package is optional
- * in that the action may be in the default package, in which case, the string "default" must be used.
- * If the action doesn't exist (or the namespace is not valid) NotFound is generated. Following the
- * action name, an "extension" is required to specify the desired content type for the response. This
- * extension is one of supported media types. An example is ".json" for a JSON response or ".html" for
- * an text/html response.
- *
- * Optionally, the result form the action may be projected based on a named property. As in
- * /web/some-namespace/some-package/some-action/some-property. If the property
- * does not exist in the result then a NotFound error is generated. A path of properties may
- * be supplied to project nested properties.
- *
- * Actions may be exposed to this web proxy by adding an annotation ("export" -> true).
- */
- def routes(user: Option[Identity])(implicit transid: TransactionId): Route = {
- (allowedOperations & webRoutePrefix) {
- validNameSegment { namespace =>
- packagePrefix { pkg =>
- validNameSegment { seg =>
- handleMatch(namespace, pkg, seg, user)
- }
- }
- }
+ }
+
+ def routes(user: Identity)(implicit transid: TransactionId): Route = routes(Some(user))
+ def routes()(implicit transid: TransactionId): Route = routes(None)
+
+ private val maxWaitForWebActionResult = Some(WhiskActionsApi.maxWaitForBlockingActivation)
+
+ /**
+ * Adds route to web based activations. Actions invoked this way are anonymous in that the
+ * caller is not authenticated. The intended action must be named in the path as a fully qualified
+ * name as in /web/some-namespace/some-package/some-action. The package is optional
+ * in that the action may be in the default package, in which case, the string "default" must be used.
+ * If the action doesn't exist (or the namespace is not valid) NotFound is generated. Following the
+ * action name, an "extension" is required to specify the desired content type for the response. This
+ * extension is one of supported media types. An example is ".json" for a JSON response or ".html" for
+ * an text/html response.
+ *
+ * Optionally, the result form the action may be projected based on a named property. As in
+ * /web/some-namespace/some-package/some-action/some-property. If the property
+ * does not exist in the result then a NotFound error is generated. A path of properties may
+ * be supplied to project nested properties.
+ *
+ * Actions may be exposed to this web proxy by adding an annotation ("export" -> true).
+ */
+ def routes(user: Option[Identity])(implicit transid: TransactionId): Route = {
+ (allowedOperations & webRoutePrefix) {
+ validNameSegment { namespace =>
+ packagePrefix { pkg =>
+ validNameSegment { seg =>
+ handleMatch(namespace, pkg, seg, user)
+ }
}
+ }
}
-
- /**
- * Gets package from datastore.
- * This method is factored out to allow mock testing.
- */
- protected def getPackage(pkgName: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[WhiskPackage] = {
- WhiskPackage.get(entityStore, pkgName.toDocId)
- }
-
- /**
- * Gets action from datastore.
- * This method is factored out to allow mock testing.
- */
- protected def getAction(actionName: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[WhiskAction] = {
- WhiskAction.get(entityStore, actionName.toDocId)
- }
-
- /**
- * Gets identity from datastore.
- * This method is factored out to allow mock testing.
- */
- protected def getIdentity(namespace: EntityName)(
- implicit transid: TransactionId): Future[Identity] = {
- Identity.get(authStore, namespace)
+ }
+
+ /**
+ * Gets package from datastore.
+ * This method is factored out to allow mock testing.
+ */
+ protected def getPackage(pkgName: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[WhiskPackage] = {
+ WhiskPackage.get(entityStore, pkgName.toDocId)
+ }
+
+ /**
+ * Gets action from datastore.
+ * This method is factored out to allow mock testing.
+ */
+ protected def getAction(actionName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Future[WhiskAction] = {
+ WhiskAction.get(entityStore, actionName.toDocId)
+ }
+
+ /**
+ * Gets identity from datastore.
+ * This method is factored out to allow mock testing.
+ */
+ protected def getIdentity(namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {
+ Identity.get(authStore, namespace)
+ }
+
+ private def handleMatch(namespaceSegment: String,
+ pkgSegment: String,
+ actionNameWithExtension: String,
+ onBehalfOf: Option[Identity])(implicit transid: TransactionId) = {
+
+ def fullyQualifiedActionName(actionName: String) = {
+ val namespace = EntityName(namespaceSegment)
+ val pkgName = if (pkgSegment == "default") None else Some(EntityName(pkgSegment))
+ namespace.addPath(pkgName).addPath(EntityName(actionName)).toFullyQualifiedEntityName
}
- private def handleMatch(namespaceSegment: String, pkgSegment: String, actionNameWithExtension: String, onBehalfOf: Option[Identity])(
- implicit transid: TransactionId) = {
-
- def fullyQualifiedActionName(actionName: String) = {
- val namespace = EntityName(namespaceSegment)
- val pkgName = if (pkgSegment == "default") None else Some(EntityName(pkgSegment))
- namespace.addPath(pkgName).addPath(EntityName(actionName)).toFullyQualifiedEntityName
- }
-
- provide(WhiskWebActionsApi.mediaTranscoderForName(actionNameWithExtension, webApiDirectives.enforceExtension)) {
- case (actionName, Some(extension)) =>
- // extract request context, checks for overrides of reserved properties, and constructs action arguments
- // as the context body which may be the incoming request when the content type is JSON or formdata, or
- // the raw body as __ow_body (and query parameters as __ow_query) otherwise
- extract(_.request.entity) { e =>
- validateSize(isWhithinRange(e.contentLengthOption.getOrElse(0)))(transid, jsonPrettyPrinter) {
- requestMethodParamsAndPath { context =>
- provide(fullyQualifiedActionName(actionName)) { fullActionName =>
- onComplete(verifyWebAction(fullActionName, onBehalfOf.isDefined)) {
- case Success((actionOwnerIdentity, action)) =>
- if (!action.annotations.asBool("web-custom-options").exists(identity)) {
- respondWithHeaders(defaultCorsResponse) {
- if (context.method == OPTIONS) {
- complete(OK, HttpEntity.Empty)
- } else {
- extractEntityAndProcessRequest(actionOwnerIdentity, action, extension, onBehalfOf, context, e)
- }
- }
- } else {
- extractEntityAndProcessRequest(actionOwnerIdentity, action, extension, onBehalfOf, context, e)
- }
-
- case Failure(t: RejectRequest) =>
- terminate(t.code, t.message)
-
- case Failure(t) =>
- logging.error(this, s"exception in handleMatch: $t")
- terminate(InternalServerError)
- }
- }
+ provide(WhiskWebActionsApi.mediaTranscoderForName(actionNameWithExtension, webApiDirectives.enforceExtension)) {
+ case (actionName, Some(extension)) =>
+ // extract request context, checks for overrides of reserved properties, and constructs action arguments
+ // as the context body which may be the incoming request when the content type is JSON or formdata, or
+ // the raw body as __ow_body (and query parameters as __ow_query) otherwise
+ extract(_.request.entity) { e =>
+ validateSize(isWhithinRange(e.contentLengthOption.getOrElse(0)))(transid, jsonPrettyPrinter) {
+ requestMethodParamsAndPath { context =>
+ provide(fullyQualifiedActionName(actionName)) { fullActionName =>
+ onComplete(verifyWebAction(fullActionName, onBehalfOf.isDefined)) {
+ case Success((actionOwnerIdentity, action)) =>
+ if (!action.annotations.asBool("web-custom-options").exists(identity)) {
+ respondWithHeaders(defaultCorsResponse) {
+ if (context.method == OPTIONS) {
+ complete(OK, HttpEntity.Empty)
+ } else {
+ extractEntityAndProcessRequest(actionOwnerIdentity, action, extension, onBehalfOf, context, e)
}
+ }
+ } else {
+ extractEntityAndProcessRequest(actionOwnerIdentity, action, extension, onBehalfOf, context, e)
}
- }
-
- case (_, None) => terminate(NotAcceptable, Messages.contentTypeExtensionNotSupported(WhiskWebActionsApi.allowedExtensions))
- }
- }
- /**
- * Checks that subject has right to post an activation and fetch the action
- * followed by the package and merge parameters. The action is fetched first since
- * it will not succeed for references relative to a binding, and the export bit is
- * confirmed before fetching the package and merging parameters.
- *
- * @return Future that completes with the action and action-owner-identity on success otherwise
- * a failed future with a request rejection error which may be one of the following:
- * not entitled (throttled), package/action not found, action not web enabled,
- * or request overrides final parameters
- */
- private def verifyWebAction(actionName: FullyQualifiedEntityName, authenticated: Boolean)(
- implicit transid: TransactionId) = {
- for {
- // lookup the identity for the action namespace
- actionOwnerIdentity <- identityLookup(actionName.path.root) flatMap {
- i => entitlementProvider.checkThrottles(i) map (_ => i)
- }
+ case Failure(t: RejectRequest) =>
+ terminate(t.code, t.message)
- // lookup the action - since actions are stored relative to package name
- // the lookup will fail if the package name for the action refers to a binding instead
- // also merge package and action parameters at the same time
- // precedence order for parameters:
- // package.params -> action.params -> query.params -> request.entity (body) -> augment arguments (namespace, path)
- action <- confirmExportedAction(actionLookup(actionName), authenticated) flatMap { a =>
- if (a.namespace.defaultPackage) {
- Future.successful(a)
- } else {
- pkgLookup(a.namespace.toFullyQualifiedEntityName) map {
- pkg => (a.inherit(pkg.parameters))
- }
+ case Failure(t) =>
+ logging.error(this, s"exception in handleMatch: $t")
+ terminate(InternalServerError)
}
+ }
}
- } yield (actionOwnerIdentity, action)
- }
-
- private def extractEntityAndProcessRequest(
- actionOwnerIdentity: Identity,
- action: WhiskAction,
- extension: MediaExtension,
- onBehalfOf: Option[Identity],
- context: Context,
- httpEntity: HttpEntity)(
- implicit transid: TransactionId) = {
-
- def process(body: Option[JsValue], isRawHttpAction: Boolean) = {
- processRequest(actionOwnerIdentity, action, extension, onBehalfOf, context.withBody(body), isRawHttpAction)
+ }
}
- provide(action.annotations.asBool("raw-http").exists(identity)) { isRawHttpAction =>
- httpEntity match {
- case Empty =>
- process(None, isRawHttpAction)
-
- case HttpEntity.Strict(ContentTypes.`application/json`, _) if !isRawHttpAction =>
- entity(as[JsObject]) { body =>
- process(Some(body), isRawHttpAction)
- }
-
- case HttpEntity.Strict(ContentType(MediaTypes.`application/x-www-form-urlencoded`, _), _) if !isRawHttpAction =>
- entity(as[FormData]) { form =>
- val body = form.fields.toMap.toJson.asJsObject
- process(Some(body), isRawHttpAction)
- }
-
- case HttpEntity.Strict(contentType, data) =>
- // application/json is not a binary type in Akka, but is binary in Spray
- if (contentType.mediaType.binary || contentType.mediaType == `application/json`) {
- Try(JsString(Base64.getEncoder.encodeToString(data.toArray))) match {
- case Success(bytes) => process(Some(bytes), isRawHttpAction)
- case Failure(t) => terminate(BadRequest, Messages.unsupportedContentType(contentType.mediaType))
- }
- } else {
- val str = JsString(data.utf8String)
- process(Some(str), isRawHttpAction)
- }
-
- case _ => terminate(BadRequest, Messages.unsupportedContentType)
- }
+ case (_, None) =>
+ terminate(NotAcceptable, Messages.contentTypeExtensionNotSupported(WhiskWebActionsApi.allowedExtensions))
+ }
+ }
+
+ /**
+ * Checks that subject has right to post an activation and fetch the action
+ * followed by the package and merge parameters. The action is fetched first since
+ * it will not succeed for references relative to a binding, and the export bit is
+ * confirmed before fetching the package and merging parameters.
+ *
+ * @return Future that completes with the action and action-owner-identity on success otherwise
+ * a failed future with a request rejection error which may be one of the following:
+ * not entitled (throttled), package/action not found, action not web enabled,
+ * or request overrides final parameters
+ */
+ private def verifyWebAction(actionName: FullyQualifiedEntityName, authenticated: Boolean)(
+ implicit transid: TransactionId) = {
+ for {
+ // lookup the identity for the action namespace
+ actionOwnerIdentity <- identityLookup(actionName.path.root) flatMap { i =>
+ entitlementProvider.checkThrottles(i) map (_ => i)
+ }
+
+ // lookup the action - since actions are stored relative to package name
+ // the lookup will fail if the package name for the action refers to a binding instead
+ // also merge package and action parameters at the same time
+ // precedence order for parameters:
+ // package.params -> action.params -> query.params -> request.entity (body) -> augment arguments (namespace, path)
+ action <- confirmExportedAction(actionLookup(actionName), authenticated) flatMap { a =>
+ if (a.namespace.defaultPackage) {
+ Future.successful(a)
+ } else {
+ pkgLookup(a.namespace.toFullyQualifiedEntityName) map { pkg =>
+ (a.inherit(pkg.parameters))
+ }
}
+ }
+ } yield (actionOwnerIdentity, action)
+ }
+
+ private def extractEntityAndProcessRequest(actionOwnerIdentity: Identity,
+ action: WhiskAction,
+ extension: MediaExtension,
+ onBehalfOf: Option[Identity],
+ context: Context,
+ httpEntity: HttpEntity)(implicit transid: TransactionId) = {
+
+ def process(body: Option[JsValue], isRawHttpAction: Boolean) = {
+ processRequest(actionOwnerIdentity, action, extension, onBehalfOf, context.withBody(body), isRawHttpAction)
}
- private def processRequest(
- actionOwnerIdentity: Identity,
- action: WhiskAction,
- responseType: MediaExtension,
- onBehalfOf: Option[Identity],
- context: Context,
- isRawHttpAction: Boolean)(
- implicit transid: TransactionId) = {
-
- def queuedActivation = {
- // checks (1) if any of the query or body parameters override final action parameters
- // computes overrides if any relative to the reserved __ow_* properties, and (2) if
- // action is a raw http handler
- //
- // NOTE: it is assumed the action parameters do not intersect with the reserved properties
- // since these are system properties, the action should not define them, and if it does,
- // they will be overwritten
- if (isRawHttpAction || context.overrides(webApiDirectives.reservedProperties ++ action.immutableParameters).isEmpty) {
- val content = context.toActionArgument(onBehalfOf, isRawHttpAction)
- invokeAction(actionOwnerIdentity, action, Some(JsObject(content)), maxWaitForWebActionResult, cause = None)
- } else {
- Future.failed(RejectRequest(BadRequest, Messages.parametersNotAllowed))
+ provide(action.annotations.asBool("raw-http").exists(identity)) { isRawHttpAction =>
+ httpEntity match {
+ case Empty =>
+ process(None, isRawHttpAction)
+
+ case HttpEntity.Strict(ContentTypes.`application/json`, _) if !isRawHttpAction =>
+ entity(as[JsObject]) { body =>
+ process(Some(body), isRawHttpAction)
+ }
+
+ case HttpEntity.Strict(ContentType(MediaTypes.`application/x-www-form-urlencoded`, _), _) if !isRawHttpAction =>
+ entity(as[FormData]) { form =>
+ val body = form.fields.toMap.toJson.asJsObject
+ process(Some(body), isRawHttpAction)
+ }
+
+ case HttpEntity.Strict(contentType, data) =>
+ // application/json is not a binary type in Akka, but is binary in Spray
+ if (contentType.mediaType.binary || contentType.mediaType == `application/json`) {
+ Try(JsString(Base64.getEncoder.encodeToString(data.toArray))) match {
+ case Success(bytes) => process(Some(bytes), isRawHttpAction)
+ case Failure(t) => terminate(BadRequest, Messages.unsupportedContentType(contentType.mediaType))
}
- }
+ } else {
+ val str = JsString(data.utf8String)
+ process(Some(str), isRawHttpAction)
+ }
- completeRequest(queuedActivation, projectResultField(context, responseType), responseType)
+ case _ => terminate(BadRequest, Messages.unsupportedContentType)
+ }
+ }
+ }
+
+ private def processRequest(actionOwnerIdentity: Identity,
+ action: WhiskAction,
+ responseType: MediaExtension,
+ onBehalfOf: Option[Identity],
+ context: Context,
+ isRawHttpAction: Boolean)(implicit transid: TransactionId) = {
+
+ def queuedActivation = {
+ // checks (1) if any of the query or body parameters override final action parameters
+ // computes overrides if any relative to the reserved __ow_* properties, and (2) if
+ // action is a raw http handler
+ //
+ // NOTE: it is assumed the action parameters do not intersect with the reserved properties
+ // since these are system properties, the action should not define them, and if it does,
+ // they will be overwritten
+ if (isRawHttpAction || context
+ .overrides(webApiDirectives.reservedProperties ++ action.immutableParameters)
+ .isEmpty) {
+ val content = context.toActionArgument(onBehalfOf, isRawHttpAction)
+ invokeAction(actionOwnerIdentity, action, Some(JsObject(content)), maxWaitForWebActionResult, cause = None)
+ } else {
+ Future.failed(RejectRequest(BadRequest, Messages.parametersNotAllowed))
+ }
}
- private def completeRequest(
- queuedActivation: Future[Either[ActivationId, WhiskActivation]],
- projectResultField: => List[String],
- responseType: MediaExtension)(
- implicit transid: TransactionId) = {
- onComplete(queuedActivation) {
- case Success(Right(activation)) =>
- val result = activation.resultAsJson
-
- if (activation.response.isSuccess || activation.response.isApplicationError) {
- val resultPath = if (activation.response.isSuccess) {
- projectResultField
- } else {
- // the activation produced an error response: therefore ignore
- // the requested projection and unwrap the error instead
- // and attempt to handle it per the desired response type (extension)
- List(ActivationResponse.ERROR_FIELD)
- }
-
- val result = getFieldPath(activation.resultAsJson, resultPath)
- result match {
- case Some(projection) =>
- val marshaler = Future(responseType.transcoder(projection, transid, webApiDirectives))
- onComplete(marshaler) {
- case Success(done) => done // all transcoders terminate the connection
- case Failure(t) => terminate(InternalServerError)
- }
- case _ => terminate(NotFound, Messages.propertyNotFound)
- }
- } else {
- terminate(BadRequest, Messages.errorProcessingRequest)
- }
-
- case Success(Left(activationId)) =>
- // blocking invoke which got queued instead
- // this should not happen, instead it should be a blocking invoke timeout
- logging.info(this, "activation waiting period expired")
- terminate(Accepted, Messages.responseNotReady)
+ completeRequest(queuedActivation, projectResultField(context, responseType), responseType)
+ }
+
+ private def completeRequest(queuedActivation: Future[Either[ActivationId, WhiskActivation]],
+ projectResultField: => List[String],
+ responseType: MediaExtension)(implicit transid: TransactionId) = {
+ onComplete(queuedActivation) {
+ case Success(Right(activation)) =>
+ val result = activation.resultAsJson
+
+ if (activation.response.isSuccess || activation.response.isApplicationError) {
+ val resultPath = if (activation.response.isSuccess) {
+ projectResultField
+ } else {
+ // the activation produced an error response: therefore ignore
+ // the requested projection and unwrap the error instead
+ // and attempt to handle it per the desired response type (extension)
+ List(ActivationResponse.ERROR_FIELD)
+ }
+
+ val result = getFieldPath(activation.resultAsJson, resultPath)
+ result match {
+ case Some(projection) =>
+ val marshaler = Future(responseType.transcoder(projection, transid, webApiDirectives))
+ onComplete(marshaler) {
+ case Success(done) => done // all transcoders terminate the connection
+ case Failure(t) => terminate(InternalServerError)
+ }
+ case _ => terminate(NotFound, Messages.propertyNotFound)
+ }
+ } else {
+ terminate(BadRequest, Messages.errorProcessingRequest)
+ }
- case Failure(t: RejectRequest) => terminate(t.code, t.message)
+ case Success(Left(activationId)) =>
+ // blocking invoke which got queued instead
+ // this should not happen, instead it should be a blocking invoke timeout
+ logging.info(this, "activation waiting period expired")
+ terminate(Accepted, Messages.responseNotReady)
- case Failure(t) =>
- logging.error(this, s"exception in completeRequest: $t")
- terminate(InternalServerError)
- }
- }
+ case Failure(t: RejectRequest) => terminate(t.code, t.message)
- /**
- * Gets package from datastore and confirms it is not a binding.
- */
- private def pkgLookup(pkg: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[WhiskPackage] = {
- getPackage(pkg).filter {
- _.binding.isEmpty
- } recoverWith {
- case _: ArtifactStoreException | DeserializationException(_, _, _) =>
- // if the package lookup fails or the package doesn't conform to expected invariants,
- // fail the request with BadRequest so as not to leak information about the existence
- // of packages that are otherwise private
- logging.info(this, s"package which does not exist")
- Future.failed(RejectRequest(NotFound))
- case _: NoSuchElementException =>
- logging.warn(this, s"'$pkg' is a binding")
- Future.failed(RejectRequest(NotFound))
- }
+ case Failure(t) =>
+ logging.error(this, s"exception in completeRequest: $t")
+ terminate(InternalServerError)
}
-
- /**
- * Gets the action if it exists and fail future with RejectRequest if it does not.
- *
- * @return future action document or NotFound rejection
- */
- private def actionLookup(actionName: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[WhiskAction] = {
- getAction(actionName) recoverWith {
- case _: ArtifactStoreException | DeserializationException(_, _, _) =>
- Future.failed(RejectRequest(NotFound))
- }
+ }
+
+ /**
+ * Gets package from datastore and confirms it is not a binding.
+ */
+ private def pkgLookup(pkg: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[WhiskPackage] = {
+ getPackage(pkg).filter {
+ _.binding.isEmpty
+ } recoverWith {
+ case _: ArtifactStoreException | DeserializationException(_, _, _) =>
+ // if the package lookup fails or the package doesn't conform to expected invariants,
+ // fail the request with BadRequest so as not to leak information about the existence
+ // of packages that are otherwise private
+ logging.info(this, s"package which does not exist")
+ Future.failed(RejectRequest(NotFound))
+ case _: NoSuchElementException =>
+ logging.warn(this, s"'$pkg' is a binding")
+ Future.failed(RejectRequest(NotFound))
}
-
- /**
- * Gets the identity for the namespace.
- */
- private def identityLookup(namespace: EntityName)(
- implicit transid: TransactionId): Future[Identity] = {
- getIdentity(namespace) recoverWith {
- case _: ArtifactStoreException | DeserializationException(_, _, _) =>
- Future.failed(RejectRequest(NotFound))
- case t =>
- // leak nothing no matter what, failure is already logged so skip here
- Future.failed(RejectRequest(NotFound))
- }
+ }
+
+ /**
+ * Gets the action if it exists and fail future with RejectRequest if it does not.
+ *
+ * @return future action document or NotFound rejection
+ */
+ private def actionLookup(actionName: FullyQualifiedEntityName)(
+ implicit transid: TransactionId): Future[WhiskAction] = {
+ getAction(actionName) recoverWith {
+ case _: ArtifactStoreException | DeserializationException(_, _, _) =>
+ Future.failed(RejectRequest(NotFound))
}
-
- /**
- * Checks if an action is exported (i.e., carries the required annotation).
- */
- private def confirmExportedAction(actionLookup: Future[WhiskAction], authenticated: Boolean)(
- implicit transid: TransactionId): Future[WhiskAction] = {
- actionLookup flatMap { action =>
- val requiresAuthenticatedUser = action.annotations.asBool("require-whisk-auth").exists(identity)
- val isExported = action.annotations.asBool("web-export").exists(identity)
-
- if ((isExported && requiresAuthenticatedUser && authenticated) ||
- (isExported && !requiresAuthenticatedUser)) {
- logging.info(this, s"${action.fullyQualifiedName(true)} is exported")
- Future.successful(action)
- } else if (!isExported) {
- logging.info(this, s"${action.fullyQualifiedName(true)} not exported")
- Future.failed(RejectRequest(NotFound))
- } else {
- logging.info(this, s"${action.fullyQualifiedName(true)} requires authentication")
- Future.failed(RejectRequest(Unauthorized))
- }
- }
+ }
+
+ /**
+ * Gets the identity for the namespace.
+ */
+ private def identityLookup(namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {
+ getIdentity(namespace) recoverWith {
+ case _: ArtifactStoreException | DeserializationException(_, _, _) =>
+ Future.failed(RejectRequest(NotFound))
+ case t =>
+ // leak nothing no matter what, failure is already logged so skip here
+ Future.failed(RejectRequest(NotFound))
}
-
- /**
- * Determines the result projection path, if any.
- *
- * @return optional list of projections
- */
- private def projectResultField(context: Context, responseType: MediaExtension): List[String] = {
- val projection = if (responseType.projectionAllowed) {
- Option(context.path)
- .filter(_.nonEmpty)
- .map(_.split("/").filter(_.nonEmpty).toList)
- .orElse(responseType.defaultProjection)
- } else responseType.defaultProjection
-
- projection.getOrElse(List())
+ }
+
+ /**
+ * Checks if an action is exported (i.e., carries the required annotation).
+ */
+ private def confirmExportedAction(actionLookup: Future[WhiskAction], authenticated: Boolean)(
+ implicit transid: TransactionId): Future[WhiskAction] = {
+ actionLookup flatMap { action =>
+ val requiresAuthenticatedUser = action.annotations.asBool("require-whisk-auth").exists(identity)
+ val isExported = action.annotations.asBool("web-export").exists(identity)
+
+ if ((isExported && requiresAuthenticatedUser && authenticated) ||
+ (isExported && !requiresAuthenticatedUser)) {
+ logging.info(this, s"${action.fullyQualifiedName(true)} is exported")
+ Future.successful(action)
+ } else if (!isExported) {
+ logging.info(this, s"${action.fullyQualifiedName(true)} not exported")
+ Future.failed(RejectRequest(NotFound))
+ } else {
+ logging.info(this, s"${action.fullyQualifiedName(true)} requires authentication")
+ Future.failed(RejectRequest(Unauthorized))
+ }
}
+ }
+
+ /**
+ * Determines the result projection path, if any.
+ *
+ * @return optional list of projections
+ */
+ private def projectResultField(context: Context, responseType: MediaExtension): List[String] = {
+ val projection = if (responseType.projectionAllowed) {
+ Option(context.path)
+ .filter(_.nonEmpty)
+ .map(_.split("/").filter(_.nonEmpty).toList)
+ .orElse(responseType.defaultProjection)
+ } else responseType.defaultProjection
+
+ projection.getOrElse(List())
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala b/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
index 03fc8be..34b3bb2 100644
--- a/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
@@ -31,37 +31,36 @@ import whisk.core.entity._
import whisk.http.Messages
protected[core] trait PostActionActivation extends PrimitiveActions with SequenceActions {
- /** The core collections require backend services to be injected in this trait. */
- services: WhiskServices =>
+ /** The core collections require backend services to be injected in this trait. */
+ services: WhiskServices =>
- /**
- * Invokes an action which may be a sequence or a primitive (single) action.
- *
- * @param user the user posting the activation
- * @param action the action to activate (parameters for packaged actions must already be merged)
- * @param payload the parameters to pass to the action
- * @param waitForResponse if not empty, wait up to specified duration for a response (this is used for blocking activations)
- * @return a future that resolves with Left(activation id) when the request is queued, or Right(activation) for a blocking request
- * which completes in time iff waiting for an response
- */
- protected[controller] def invokeAction(
- user: Identity,
- action: WhiskAction,
- payload: Option[JsObject],
- waitForResponse: Option[FiniteDuration],
- cause: Option[ActivationId])(
- implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {
- action.toExecutableWhiskAction match {
- // this is a topmost sequence
- case None =>
- val SequenceExec(components) = action.exec
- invokeSequence(user, action, components, payload, waitForResponse, cause, topmost = true, 0).map(r => r._1)
- // a non-deprecated ExecutableWhiskAction
- case Some(executable) if !executable.exec.deprecated =>
- invokeSingleAction(user, executable, payload, waitForResponse, cause)
- // a deprecated exec
- case _ =>
- Future.failed(RejectRequest(BadRequest, Messages.runtimeDeprecated(action.exec)))
- }
+ /**
+ * Invokes an action which may be a sequence or a primitive (single) action.
+ *
+ * @param user the user posting the activation
+ * @param action the action to activate (parameters for packaged actions must already be merged)
+ * @param payload the parameters to pass to the action
+ * @param waitForResponse if not empty, wait up to specified duration for a response (this is used for blocking activations)
+ * @return a future that resolves with Left(activation id) when the request is queued, or Right(activation) for a blocking request
+ * which completes in time iff waiting for an response
+ */
+ protected[controller] def invokeAction(
+ user: Identity,
+ action: WhiskAction,
+ payload: Option[JsObject],
+ waitForResponse: Option[FiniteDuration],
+ cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {
+ action.toExecutableWhiskAction match {
+ // this is a topmost sequence
+ case None =>
+ val SequenceExec(components) = action.exec
+ invokeSequence(user, action, components, payload, waitForResponse, cause, topmost = true, 0).map(r => r._1)
+ // a non-deprecated ExecutableWhiskAction
+ case Some(executable) if !executable.exec.deprecated =>
+ invokeSingleAction(user, executable, payload, waitForResponse, cause)
+ // a deprecated exec
+ case _ =>
+ Future.failed(RejectRequest(BadRequest, Messages.runtimeDeprecated(action.exec)))
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala b/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
index 8fee625..f159192 100644
--- a/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
@@ -43,271 +43,272 @@ import whisk.core.entity.types.EntityStore
import whisk.utils.ExecutionContextFactory.FutureExtensions
protected[actions] trait PrimitiveActions {
- /** The core collections require backend services to be injected in this trait. */
- services: WhiskServices =>
-
- /** An actor system for timed based futures. */
- protected implicit val actorSystem: ActorSystem
-
- /** An execution context for futures. */
- protected implicit val executionContext: ExecutionContext
-
- protected implicit val logging: Logging
-
- /**
- * The index of the active ack topic, this controller is listening for.
- * Typically this is also the instance number of the controller
- */
- protected val activeAckTopicIndex: InstanceId
-
- /** Database service to CRUD actions. */
- protected val entityStore: EntityStore
-
- /** Database service to get activations. */
- protected val activationStore: ActivationStore
-
- /**
- * Posts request to the loadbalancer. If the loadbalancer accepts the requests with an activation id,
- * then wait for the result of the activation if necessary.
- *
- * NOTE:
- * For activations of actions, cause is populated only for actions that were invoked as a result of a sequence activation.
- * For actions that are enclosed in a sequence and are activated as a result of the sequence activation, the cause
- * contains the activation id of the immediately enclosing sequence.
- * e.g.,: s -> a, x, c and x -> c (x and s are sequences, a, b, c atomic actions)
- * cause for a, x, c is the activation id of s
- * cause for c is the activation id of x
- * cause for s is not defined
- *
- * @param user the identity invoking the action
- * @param action the action to invoke
- * @param payload the dynamic arguments for the activation
- * @param waitForResponse if not empty, wait upto specified duration for a response (this is used for blocking activations)
- * @param cause the activation id that is responsible for this invoke/activation
- * @param transid a transaction id for logging
- * @return a promise that completes with one of the following successful cases:
- * Right(WhiskActivation) if waiting for a response and response is ready within allowed duration,
- * Left(ActivationId) if not waiting for a response, or allowed duration has elapsed without a result ready
- * or these custom failures:
- * RequestEntityTooLarge if the message is too large to to post to the message bus
- */
- protected[actions] def invokeSingleAction(
- user: Identity,
- action: ExecutableWhiskAction,
- payload: Option[JsObject],
- waitForResponse: Option[FiniteDuration],
- cause: Option[ActivationId])(
- implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {
-
- // merge package parameters with action (action parameters supersede), then merge in payload
- val args = action.parameters merge payload
- val message = ActivationMessage(
- transid,
- FullyQualifiedEntityName(action.namespace, action.name, Some(action.version)),
- action.rev,
- user,
- activationIdFactory.make(), // activation id created here
- activationNamespace = user.namespace.toPath,
- activeAckTopicIndex,
- waitForResponse.isDefined,
- args,
- cause = cause)
-
- val startActivation = transid.started(this, waitForResponse.map(_ => LoggingMarkers.CONTROLLER_ACTIVATION_BLOCKING).getOrElse(LoggingMarkers.CONTROLLER_ACTIVATION))
- val startLoadbalancer = transid.started(this, LoggingMarkers.CONTROLLER_LOADBALANCER, s"action activation id: ${message.activationId}")
- val postedFuture = loadBalancer.publish(action, message)
-
- postedFuture.flatMap { activeAckResponse =>
- // successfully posted activation request to the message bus
- transid.finished(this, startLoadbalancer)
-
- // is caller waiting for the result of the activation?
- waitForResponse.map { timeout =>
- // yes, then wait for the activation response from the message bus
- // (known as the active response or active ack)
- waitForActivationResponse(user, message.activationId, timeout, activeAckResponse)
- .andThen { case _ => transid.finished(this, startActivation) }
- }.getOrElse {
- // no, return the activation id
- transid.finished(this, startActivation)
- Future.successful(Left(message.activationId))
- }
+ /** The core collections require backend services to be injected in this trait. */
+ services: WhiskServices =>
+
+ /** An actor system for timed based futures. */
+ protected implicit val actorSystem: ActorSystem
+
+ /** An execution context for futures. */
+ protected implicit val executionContext: ExecutionContext
+
+ protected implicit val logging: Logging
+
+ /**
+ * The index of the active ack topic, this controller is listening for.
+ * Typically this is also the instance number of the controller
+ */
+ protected val activeAckTopicIndex: InstanceId
+
+ /** Database service to CRUD actions. */
+ protected val entityStore: EntityStore
+
+ /** Database service to get activations. */
+ protected val activationStore: ActivationStore
+
+ /**
+ * Posts request to the loadbalancer. If the loadbalancer accepts the requests with an activation id,
+ * then wait for the result of the activation if necessary.
+ *
+ * NOTE:
+ * For activations of actions, cause is populated only for actions that were invoked as a result of a sequence activation.
+ * For actions that are enclosed in a sequence and are activated as a result of the sequence activation, the cause
+ * contains the activation id of the immediately enclosing sequence.
+ * e.g.,: s -> a, x, c and x -> c (x and s are sequences, a, b, c atomic actions)
+ * cause for a, x, c is the activation id of s
+ * cause for c is the activation id of x
+ * cause for s is not defined
+ *
+ * @param user the identity invoking the action
+ * @param action the action to invoke
+ * @param payload the dynamic arguments for the activation
+ * @param waitForResponse if not empty, wait upto specified duration for a response (this is used for blocking activations)
+ * @param cause the activation id that is responsible for this invoke/activation
+ * @param transid a transaction id for logging
+ * @return a promise that completes with one of the following successful cases:
+ * Right(WhiskActivation) if waiting for a response and response is ready within allowed duration,
+ * Left(ActivationId) if not waiting for a response, or allowed duration has elapsed without a result ready
+ * or these custom failures:
+ * RequestEntityTooLarge if the message is too large to to post to the message bus
+ */
+ protected[actions] def invokeSingleAction(
+ user: Identity,
+ action: ExecutableWhiskAction,
+ payload: Option[JsObject],
+ waitForResponse: Option[FiniteDuration],
+ cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {
+
+ // merge package parameters with action (action parameters supersede), then merge in payload
+ val args = action.parameters merge payload
+ val message = ActivationMessage(
+ transid,
+ FullyQualifiedEntityName(action.namespace, action.name, Some(action.version)),
+ action.rev,
+ user,
+ activationIdFactory.make(), // activation id created here
+ activationNamespace = user.namespace.toPath,
+ activeAckTopicIndex,
+ waitForResponse.isDefined,
+ args,
+ cause = cause)
+
+ val startActivation = transid.started(
+ this,
+ waitForResponse
+ .map(_ => LoggingMarkers.CONTROLLER_ACTIVATION_BLOCKING)
+ .getOrElse(LoggingMarkers.CONTROLLER_ACTIVATION))
+ val startLoadbalancer =
+ transid.started(this, LoggingMarkers.CONTROLLER_LOADBALANCER, s"action activation id: ${message.activationId}")
+ val postedFuture = loadBalancer.publish(action, message)
+
+ postedFuture.flatMap { activeAckResponse =>
+ // successfully posted activation request to the message bus
+ transid.finished(this, startLoadbalancer)
+
+ // is caller waiting for the result of the activation?
+ waitForResponse
+ .map { timeout =>
+ // yes, then wait for the activation response from the message bus
+ // (known as the active response or active ack)
+ waitForActivationResponse(user, message.activationId, timeout, activeAckResponse)
+ .andThen { case _ => transid.finished(this, startActivation) }
}
+ .getOrElse {
+ // no, return the activation id
+ transid.finished(this, startActivation)
+ Future.successful(Left(message.activationId))
+ }
+ }
+ }
+
+ /**
+ * Waits for a response from the message bus (e.g., Kafka) containing the result of the activation. This is the fast path
+ * used for blocking calls where only the result of the activation is needed. This path is called active acknowledgement
+ * or active ack.
+ *
+ * While waiting for the active ack, periodically poll the datastore in case there is a failure in the fast path delivery
+ * which could happen if the connection from an invoker to the message bus is disrupted, or if the publishing of the response
+ * fails because the message is too large.
+ */
+ private def waitForActivationResponse(user: Identity,
+ activationId: ActivationId,
+ totalWaitTime: FiniteDuration,
+ activeAckResponse: Future[Either[ActivationId, WhiskActivation]])(
+ implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {
+ // this is the promise which active ack or db polling will try to complete via:
+ // 1. active ack response, or
+ // 2. failing active ack (due to active ack timeout), fall over to db polling
+ // 3. timeout on db polling => converts activation to non-blocking (returns activation id only)
+ // 4. internal error message
+ val docid = DocId(WhiskEntity.qualifiedName(user.namespace.toPath, activationId))
+ val (promise, finisher) = ActivationFinisher.props({ () =>
+ WhiskActivation.get(activationStore, docid)
+ })
+
+ logging.info(this, s"action activation will block for result upto $totalWaitTime")
+
+ activeAckResponse map {
+ case result @ Right(_) =>
+ // activation complete, result is available
+ finisher ! ActivationFinisher.Finish(result)
+
+ case _ =>
+ // active ack received but it does not carry the response,
+ // no result available except by polling the db
+ logging.warn(this, "pre-emptively polling db because active ack is missing result")
+ finisher ! Scheduler.WorkOnceNow
}
- /**
- * Waits for a response from the message bus (e.g., Kafka) containing the result of the activation. This is the fast path
- * used for blocking calls where only the result of the activation is needed. This path is called active acknowledgement
- * or active ack.
- *
- * While waiting for the active ack, periodically poll the datastore in case there is a failure in the fast path delivery
- * which could happen if the connection from an invoker to the message bus is disrupted, or if the publishing of the response
- * fails because the message is too large.
- */
- private def waitForActivationResponse(
- user: Identity,
- activationId: ActivationId,
- totalWaitTime: FiniteDuration,
- activeAckResponse: Future[Either[ActivationId, WhiskActivation]])(
- implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {
- // this is the promise which active ack or db polling will try to complete via:
- // 1. active ack response, or
- // 2. failing active ack (due to active ack timeout), fall over to db polling
- // 3. timeout on db polling => converts activation to non-blocking (returns activation id only)
- // 4. internal error message
- val docid = DocId(WhiskEntity.qualifiedName(user.namespace.toPath, activationId))
- val (promise, finisher) = ActivationFinisher.props({
- () => WhiskActivation.get(activationStore, docid)
- })
-
- logging.info(this, s"action activation will block for result upto $totalWaitTime")
-
- activeAckResponse map {
- case result @ Right(_) =>
- // activation complete, result is available
- finisher ! ActivationFinisher.Finish(result)
-
- case _ =>
- // active ack received but it does not carry the response,
- // no result available except by polling the db
- logging.warn(this, "pre-emptively polling db because active ack is missing result")
- finisher ! Scheduler.WorkOnceNow
+ // return the promise which is either fulfilled by active ack, polling from the database,
+ // or the timeout alternative when the allowed duration expires (i.e., the action took
+ // longer than the permitted, per totalWaitTime).
+ promise.withAlternativeAfterTimeout(
+ totalWaitTime, {
+ Future.successful(Left(activationId)).andThen {
+ // result no longer interesting; terminate the finisher/shut down db polling if necessary
+ case _ => actorSystem.stop(finisher)
}
-
- // return the promise which is either fulfilled by active ack, polling from the database,
- // or the timeout alternative when the allowed duration expires (i.e., the action took
- // longer than the permitted, per totalWaitTime).
- promise.withAlternativeAfterTimeout(totalWaitTime, {
- Future.successful(Left(activationId)).andThen {
- // result no longer interesting; terminate the finisher/shut down db polling if necessary
- case _ => actorSystem.stop(finisher)
- }
- })
- }
+ })
+ }
}
/** Companion to the ActivationFinisher. */
protected[actions] object ActivationFinisher {
- case class Finish(activation: Right[ActivationId, WhiskActivation])
-
- private type ActivationLookup = () => Future[WhiskActivation]
-
- /** Periodically polls the db to cover missing active acks. */
- private val datastorePollPeriodForActivation = 15.seconds
-
- /**
- * In case of a partial active ack where it is know an activation completed
- * but the result could not be sent over the bus, use this periodicity to poll
- * for a result.
- */
- private val datastorePreemptivePolling = Seq(1.second, 3.seconds, 5.seconds, 7.seconds)
-
- def props(activationLookup: ActivationLookup)(
- implicit transid: TransactionId,
- actorSystem: ActorSystem,
- executionContext: ExecutionContext,
- logging: Logging): (Future[Either[ActivationId, WhiskActivation]], ActorRef) = {
-
- val (p, _, f) = props(activationLookup, datastorePollPeriodForActivation, datastorePreemptivePolling)
- (p.future, f) // hides the polling actor
- }
-
- /**
- * Creates the finishing actor.
- * This is factored for testing.
- */
- protected[actions] def props(
- activationLookup: ActivationLookup,
- slowPoll: FiniteDuration,
- fastPolls: Seq[FiniteDuration])(
- implicit transid: TransactionId,
- actorSystem: ActorSystem,
- executionContext: ExecutionContext,
- logging: Logging): (Promise[Either[ActivationId, WhiskActivation]], ActorRef, ActorRef) = {
-
- // this is strictly completed by the finishing actor
- val promise = Promise[Either[ActivationId, WhiskActivation]]
- val dbpoller = poller(slowPoll, promise, activationLookup)
- val finisher = Props(new ActivationFinisher(dbpoller, fastPolls, promise))
-
- (promise, dbpoller, actorSystem.actorOf(finisher))
- }
-
- /**
- * An actor to complete a blocking activation request. It encapsulates a promise
- * to be completed when the result is ready. This may happen in one of two ways.
- * An active ack message is relayed to this actor to complete the promise when
- * the active ack is received. Or in case of a partial/missing active ack, an
- * explicitly scheduled datastore poll of the activation record, if found, will
- * complete the transaction. When the promise is fulfilled, the actor self destructs.
- */
- private class ActivationFinisher(
- poller: ActorRef, // the activation poller
- fastPollPeriods: Seq[FiniteDuration],
- promise: Promise[Either[ActivationId, WhiskActivation]])(
- implicit transid: TransactionId,
- actorSystem: ActorSystem,
- executionContext: ExecutionContext,
- logging: Logging) extends Actor {
-
- // when the future completes, self-destruct
- promise.future.andThen { case _ => shutdown() }
-
- val preemptiveMsgs: Buffer[Cancellable] = Buffer.empty
-
- def receive = {
- case ActivationFinisher.Finish(activation) =>
- promise.trySuccess(activation)
-
- case msg @ Scheduler.WorkOnceNow =>
- // try up to three times when pre-emptying the schedule
- fastPollPeriods.foreach {
- s => preemptiveMsgs += context.system.scheduler.scheduleOnce(s, poller, msg)
- }
- }
-
- def shutdown(): Unit = {
- preemptiveMsgs.foreach(_.cancel())
- preemptiveMsgs.clear()
- context.stop(poller)
- context.stop(self)
+ case class Finish(activation: Right[ActivationId, WhiskActivation])
+
+ private type ActivationLookup = () => Future[WhiskActivation]
+
+ /** Periodically polls the db to cover missing active acks. */
+ private val datastorePollPeriodForActivation = 15.seconds
+
+ /**
+ * In case of a partial active ack where it is know an activation completed
+ * but the result could not be sent over the bus, use this periodicity to poll
+ * for a result.
+ */
+ private val datastorePreemptivePolling = Seq(1.second, 3.seconds, 5.seconds, 7.seconds)
+
+ def props(activationLookup: ActivationLookup)(
+ implicit transid: TransactionId,
+ actorSystem: ActorSystem,
+ executionContext: ExecutionContext,
+ logging: Logging): (Future[Either[ActivationId, WhiskActivation]], ActorRef) = {
+
+ val (p, _, f) = props(activationLookup, datastorePollPeriodForActivation, datastorePreemptivePolling)
+ (p.future, f) // hides the polling actor
+ }
+
+ /**
+ * Creates the finishing actor.
+ * This is factored for testing.
+ */
+ protected[actions] def props(activationLookup: ActivationLookup,
+ slowPoll: FiniteDuration,
+ fastPolls: Seq[FiniteDuration])(
+ implicit transid: TransactionId,
+ actorSystem: ActorSystem,
+ executionContext: ExecutionContext,
+ logging: Logging): (Promise[Either[ActivationId, WhiskActivation]], ActorRef, ActorRef) = {
+
+ // this is strictly completed by the finishing actor
+ val promise = Promise[Either[ActivationId, WhiskActivation]]
+ val dbpoller = poller(slowPoll, promise, activationLookup)
+ val finisher = Props(new ActivationFinisher(dbpoller, fastPolls, promise))
+
+ (promise, dbpoller, actorSystem.actorOf(finisher))
+ }
+
+ /**
+ * An actor to complete a blocking activation request. It encapsulates a promise
+ * to be completed when the result is ready. This may happen in one of two ways.
+ * An active ack message is relayed to this actor to complete the promise when
+ * the active ack is received. Or in case of a partial/missing active ack, an
+ * explicitly scheduled datastore poll of the activation record, if found, will
+ * complete the transaction. When the promise is fulfilled, the actor self destructs.
+ */
+ private class ActivationFinisher(poller: ActorRef, // the activation poller
+ fastPollPeriods: Seq[FiniteDuration],
+ promise: Promise[Either[ActivationId, WhiskActivation]])(
+ implicit transid: TransactionId,
+ actorSystem: ActorSystem,
+ executionContext: ExecutionContext,
+ logging: Logging)
+ extends Actor {
+
+ // when the future completes, self-destruct
+ promise.future.andThen { case _ => shutdown() }
+
+ val preemptiveMsgs: Buffer[Cancellable] = Buffer.empty
+
+ def receive = {
+ case ActivationFinisher.Finish(activation) =>
+ promise.trySuccess(activation)
+
+ case msg @ Scheduler.WorkOnceNow =>
+ // try up to three times when pre-emptying the schedule
+ fastPollPeriods.foreach { s =>
+ preemptiveMsgs += context.system.scheduler.scheduleOnce(s, poller, msg)
}
+ }
- override def postStop() = {
- logging.info(this, "finisher shutdown")
- preemptiveMsgs.foreach(_.cancel())
- preemptiveMsgs.clear()
- context.stop(poller)
- }
+ def shutdown(): Unit = {
+ preemptiveMsgs.foreach(_.cancel())
+ preemptiveMsgs.clear()
+ context.stop(poller)
+ context.stop(self)
}
- /**
- * This creates the inner datastore poller for the completed activation.
- * It is a factory method to facilitate testing.
- */
- private def poller(
- slowPollPeriod: FiniteDuration,
- promise: Promise[Either[ActivationId, WhiskActivation]],
- activationLookup: ActivationLookup)(
- implicit transid: TransactionId,
- actorSystem: ActorSystem,
- executionContext: ExecutionContext,
- logging: Logging): ActorRef = {
- Scheduler.scheduleWaitAtMost(
- slowPollPeriod,
- initialDelay = slowPollPeriod,
- name = "dbpoll")(() => {
- if (!promise.isCompleted) {
- activationLookup() map {
- // complete the future, which in turn will poison pill this scheduler
- activation => promise.trySuccess(Right(activation.withoutLogs)) // logs excluded on blocking calls
- } andThen {
- case Failure(e: NoDocumentException) => // do nothing, scheduler will reschedule another poll
- case Failure(t: Throwable) => // something went wrong, abort
- logging.error(this, s"failed while waiting on result: ${t.getMessage}")
- promise.tryFailure(t) // complete the future, which in turn will poison pill this scheduler
- }
- } else Future.successful({}) // the scheduler will be halted because the promise is now resolved
- })
+ override def postStop() = {
+ logging.info(this, "finisher shutdown")
+ preemptiveMsgs.foreach(_.cancel())
+ preemptiveMsgs.clear()
+ context.stop(poller)
}
+ }
+
+ /**
+ * This creates the inner datastore poller for the completed activation.
+ * It is a factory method to facilitate testing.
+ */
+ private def poller(slowPollPeriod: FiniteDuration,
+ promise: Promise[Either[ActivationId, WhiskActivation]],
+ activationLookup: ActivationLookup)(implicit transid: TransactionId,
+ actorSystem: ActorSystem,
+ executionContext: ExecutionContext,
+ logging: Logging): ActorRef = {
+ Scheduler.scheduleWaitAtMost(slowPollPeriod, initialDelay = slowPollPeriod, name = "dbpoll")(() => {
+ if (!promise.isCompleted) {
+ activationLookup() map {
+ // complete the future, which in turn will poison pill this scheduler
+ activation =>
+ promise.trySuccess(Right(activation.withoutLogs)) // logs excluded on blocking calls
+ } andThen {
+ case Failure(e: NoDocumentException) => // do nothing, scheduler will reschedule another poll
+ case Failure(t: Throwable) => // something went wrong, abort
+ logging.error(this, s"failed while waiting on result: ${t.getMessage}")
+ promise.tryFailure(t) // complete the future, which in turn will poison pill this scheduler
+ }
+ } else Future.successful({}) // the scheduler will be halted because the promise is now resolved
+ })
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala b/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
index e60e1dd..92b2b80 100644
--- a/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
@@ -43,317 +43,342 @@ import whisk.http.Messages._
import whisk.utils.ExecutionContextFactory.FutureExtensions
protected[actions] trait SequenceActions {
- /** The core collections require backend services to be injected in this trait. */
- services: WhiskServices =>
-
- /** An actor system for timed based futures. */
- protected implicit val actorSystem: ActorSystem
-
- /** An execution context for futures. */
- protected implicit val executionContext: ExecutionContext
-
- protected implicit val logging: Logging
-
- /** Database service to CRUD actions. */
- protected val entityStore: EntityStore
-
- /** Database service to get activations. */
- protected val activationStore: ActivationStore
-
- /** A method that knows how to invoke a single primitive action. */
- protected[actions] def invokeAction(
- user: Identity,
- action: WhiskAction,
- payload: Option[JsObject],
- waitForResponse: Option[FiniteDuration],
- cause: Option[ActivationId])(
- implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]]
-
- /**
- * Executes a sequence by invoking in a blocking fashion each of its components.
- *
- * @param user the user invoking the action
- * @param action the sequence action to be invoked
- * @param payload the dynamic arguments for the activation
- * @param blocking true iff this is a blocking invoke
- * @param topmost true iff this is the topmost sequence invoked directly through the api (not indirectly through a sequence)
- * @param components the actions in the sequence
- * @param cause the id of the activation that caused this sequence (defined only for inner sequences and None for topmost sequences)
- * @param atomicActionsCount the dynamic atomic action count observed so far since the start of invocation of the topmost sequence(0 if topmost)
- * @param transid a transaction id for logging
- * @return a future of type (ActivationId, Some(WhiskActivation), atomicActionsCount) if blocking; else (ActivationId, None, 0)
- */
- protected[actions] def invokeSequence(
- user: Identity,
- action: WhiskAction,
- components: Vector[FullyQualifiedEntityName],
- payload: Option[JsObject],
- waitForOutermostResponse: Option[FiniteDuration],
- cause: Option[ActivationId],
- topmost: Boolean,
- atomicActionsCount: Int)(
- implicit transid: TransactionId): Future[(Either[ActivationId, WhiskActivation], Int)] = {
- require(action.exec.kind == Exec.SEQUENCE, "this method requires an action sequence")
-
- // create new activation id that corresponds to the sequence
- val seqActivationId = activationIdFactory.make()
- logging.info(this, s"invoking sequence $action topmost $topmost activationid '$seqActivationId'")
-
- val start = Instant.now(Clock.systemUTC())
- val futureSeqResult: Future[(Either[ActivationId, WhiskActivation], Int)] = {
- // even though the result of completeSequenceActivation is Right[WhiskActivation],
- // use a more general type for futureSeqResult in case a blocking invoke takes
- // longer than expected and we must return Left[ActivationId] instead
- completeSequenceActivation(
- seqActivationId,
- // the cause for the component activations is the current sequence
- invokeSequenceComponents(user, action, seqActivationId, payload, components, cause = Some(seqActivationId), atomicActionsCount),
- user, action, topmost, start, cause)
- }
-
- if (topmost) { // need to deal with blocking and closing connection
- waitForOutermostResponse.map { timeout =>
- logging.info(this, s"invoke sequence blocking topmost!")
- futureSeqResult.withAlternativeAfterTimeout(timeout, Future.successful(Left(seqActivationId), atomicActionsCount))
- }.getOrElse {
- // non-blocking sequence execution, return activation id
- Future.successful(Left(seqActivationId), 0)
- }
- } else {
- // not topmost, no need to worry about terminating incoming request
- // and this is a blocking activation therefore by definition
- // Note: the future for the sequence result recovers from all throwable failures
- futureSeqResult
- }
+ /** The core collections require backend services to be injected in this trait. */
+ services: WhiskServices =>
+
+ /** An actor system for timed based futures. */
+ protected implicit val actorSystem: ActorSystem
+
+ /** An execution context for futures. */
+ protected implicit val executionContext: ExecutionContext
+
+ protected implicit val logging: Logging
+
+ /** Database service to CRUD actions. */
+ protected val entityStore: EntityStore
+
+ /** Database service to get activations. */
+ protected val activationStore: ActivationStore
+
+ /** A method that knows how to invoke a single primitive action. */
+ protected[actions] def invokeAction(
+ user: Identity,
+ action: WhiskAction,
+ payload: Option[JsObject],
+ waitForResponse: Option[FiniteDuration],
+ cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]]
+
+ /**
+ * Executes a sequence by invoking in a blocking fashion each of its components.
+ *
+ * @param user the user invoking the action
+ * @param action the sequence action to be invoked
+ * @param payload the dynamic arguments for the activation
+ * @param blocking true iff this is a blocking invoke
+ * @param topmost true iff this is the topmost sequence invoked directly through the api (not indirectly through a sequence)
+ * @param components the actions in the sequence
+ * @param cause the id of the activation that caused this sequence (defined only for inner sequences and None for topmost sequences)
+ * @param atomicActionsCount the dynamic atomic action count observed so far since the start of invocation of the topmost sequence(0 if topmost)
+ * @param transid a transaction id for logging
+ * @return a future of type (ActivationId, Some(WhiskActivation), atomicActionsCount) if blocking; else (ActivationId, None, 0)
+ */
+ protected[actions] def invokeSequence(
+ user: Identity,
+ action: WhiskAction,
+ components: Vector[FullyQualifiedEntityName],
+ payload: Option[JsObject],
+ waitForOutermostResponse: Option[FiniteDuration],
+ cause: Option[ActivationId],
+ topmost: Boolean,
+ atomicActionsCount: Int)(implicit transid: TransactionId): Future[(Either[ActivationId, WhiskActivation], Int)] = {
+ require(action.exec.kind == Exec.SEQUENCE, "this method requires an action sequence")
+
+ // create new activation id that corresponds to the sequence
+ val seqActivationId = activationIdFactory.make()
+ logging.info(this, s"invoking sequence $action topmost $topmost activationid '$seqActivationId'")
+
+ val start = Instant.now(Clock.systemUTC())
+ val futureSeqResult: Future[(Either[ActivationId, WhiskActivation], Int)] = {
+ // even though the result of completeSequenceActivation is Right[WhiskActivation],
+ // use a more general type for futureSeqResult in case a blocking invoke takes
+ // longer than expected and we must return Left[ActivationId] instead
+ completeSequenceActivation(
+ seqActivationId,
+ // the cause for the component activations is the current sequence
+ invokeSequenceComponents(
+ user,
+ action,
+ seqActivationId,
+ payload,
+ components,
+ cause = Some(seqActivationId),
+ atomicActionsCount),
+ user,
+ action,
+ topmost,
+ start,
+ cause)
}
- /**
- * Creates an activation for the sequence and writes it back to the datastore.
- */
- private def completeSequenceActivation(
- seqActivationId: ActivationId,
- futureSeqResult: Future[SequenceAccounting],
- user: Identity,
- action: WhiskAction,
- topmost: Boolean,
- start: Instant,
- cause: Option[ActivationId])(
- implicit transid: TransactionId): Future[(Right[ActivationId, WhiskActivation], Int)] = {
- // not topmost, no need to worry about terminating incoming request
- // Note: the future for the sequence result recovers from all throwable failures
- futureSeqResult.map { accounting =>
- // sequence terminated, the result of the sequence is the result of the last completed activation
- val end = Instant.now(Clock.systemUTC())
- val seqActivation = makeSequenceActivation(user, action, seqActivationId, accounting, topmost, cause, start, end)
- (Right(seqActivation), accounting.atomicActionCnt)
- }.andThen {
- case Success((Right(seqActivation), _)) => storeSequenceActivation(seqActivation)
-
- // This should never happen; in this case, there is no activation record created or stored:
- // should there be?
- case Failure(t) => logging.error(this, s"Sequence activation failed: ${t.getMessage}")
+ if (topmost) { // need to deal with blocking and closing connection
+ waitForOutermostResponse
+ .map { timeout =>
+ logging.info(this, s"invoke sequence blocking topmost!")
+ futureSeqResult.withAlternativeAfterTimeout(
+ timeout,
+ Future.successful(Left(seqActivationId), atomicActionsCount))
}
- }
-
- /**
- * Stores sequence activation to database.
- */
- private def storeSequenceActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {
- logging.info(this, s"recording activation '${activation.activationId}'")
- WhiskActivation.put(activationStore, activation)(transid, notifier = None) onComplete {
- case Success(id) => logging.info(this, s"recorded activation")
- case Failure(t) => logging.error(this, s"failed to record activation ${activation.activationId} with error ${t.getLocalizedMessage}")
+ .getOrElse {
+ // non-blocking sequence execution, return activation id
+ Future.successful(Left(seqActivationId), 0)
}
+ } else {
+ // not topmost, no need to worry about terminating incoming request
+ // and this is a blocking activation therefore by definition
+ // Note: the future for the sequence result recovers from all throwable failures
+ futureSeqResult
}
-
- /**
- * Creates an activation for a sequence.
- */
- private def makeSequenceActivation(
- user: Identity,
- action: WhiskAction,
- activationId: ActivationId,
- accounting: SequenceAccounting,
- topmost: Boolean,
- cause: Option[ActivationId],
- start: Instant,
- end: Instant): WhiskActivation = {
-
- // compute max memory
- val sequenceLimits = accounting.maxMemory map {
- maxMemoryAcrossActionsInSequence =>
- Parameters("limits", ActionLimits(
- action.limits.timeout,
- MemoryLimit(maxMemoryAcrossActionsInSequence MB),
- action.limits.logs).toJson)
- } getOrElse (Parameters())
-
- // set causedBy if not topmost sequence
- val causedBy = if (!topmost) {
- Parameters("causedBy", JsString("sequence"))
- } else {
- Parameters()
- }
-
- // create the whisk activation
- WhiskActivation(
- namespace = user.namespace.toPath,
- name = action.name,
- user.subject,
- activationId = activationId,
- start = start,
- end = end,
- cause = if (topmost) None else cause, // propagate the cause for inner sequences, but undefined for topmost
- response = accounting.previousResponse.getAndSet(null), // getAndSet(null) drops reference to the activation result
- logs = accounting.finalLogs,
- version = action.version,
- publish = false,
- annotations = Parameters("topmost", JsBoolean(topmost)) ++
- Parameters("path", action.fullyQualifiedName(false).toString) ++
- Parameters("kind", "sequence") ++
- causedBy ++
- sequenceLimits,
- duration = Some(accounting.duration))
+ }
+
+ /**
+ * Creates an activation for the sequence and writes it back to the datastore.
+ */
+ private def completeSequenceActivation(seqActivationId: ActivationId,
+ futureSeqResult: Future[SequenceAccounting],
+ user: Identity,
+ action: WhiskAction,
+ topmost: Boolean,
+ start: Instant,
+ cause: Option[ActivationId])(
+ implicit transid: TransactionId): Future[(Right[ActivationId, WhiskActivation], Int)] = {
+ // not topmost, no need to worry about terminating incoming request
+ // Note: the future for the sequence result recovers from all throwable failures
+ futureSeqResult
+ .map { accounting =>
+ // sequence terminated, the result of the sequence is the result of the last completed activation
+ val end = Instant.now(Clock.systemUTC())
+ val seqActivation =
+ makeSequenceActivation(user, action, seqActivationId, accounting, topmost, cause, start, end)
+ (Right(seqActivation), accounting.atomicActionCnt)
+ }
+ .andThen {
+ case Success((Right(seqActivation), _)) => storeSequenceActivation(seqActivation)
+
+ // This should never happen; in this case, there is no activation record created or stored:
+ // should there be?
+ case Failure(t) => logging.error(this, s"Sequence activation failed: ${t.getMessage}")
+ }
+ }
+
+ /**
+ * Stores sequence activation to database.
+ */
+ private def storeSequenceActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {
+ logging.info(this, s"recording activation '${activation.activationId}'")
+ WhiskActivation.put(activationStore, activation)(transid, notifier = None) onComplete {
+ case Success(id) => logging.info(this, s"recorded activation")
+ case Failure(t) =>
+ logging.error(
+ this,
+ s"failed to record activation ${activation.activationId} with error ${t.getLocalizedMessage}")
+ }
+ }
+
+ /**
+ * Creates an activation for a sequence.
+ */
+ private def makeSequenceActivation(user: Identity,
+ action: WhiskAction,
+ activationId: ActivationId,
+ accounting: SequenceAccounting,
+ topmost: Boolean,
+ cause: Option[ActivationId],
+ start: Instant,
+ end: Instant): WhiskActivation = {
+
+ // compute max memory
+ val sequenceLimits = accounting.maxMemory map { maxMemoryAcrossActionsInSequence =>
+ Parameters(
+ "limits",
+ ActionLimits(action.limits.timeout, MemoryLimit(maxMemoryAcrossActionsInSequence MB), action.limits.logs).toJson)
+ } getOrElse (Parameters())
+
+ // set causedBy if not topmost sequence
+ val causedBy = if (!topmost) {
+ Parameters("causedBy", JsString("sequence"))
+ } else {
+ Parameters()
}
- /**
- * Invokes the components of a sequence in a blocking fashion.
- * Returns a vector of successful futures containing the results of the invocation of all components in the sequence.
- * Unexpected behavior is modeled through an Either with activation(right) or activation response in case of error (left).
- *
- * Keeps track of the dynamic atomic action count.
- * @param user the user invoking the sequence
- * @param seqAction the sequence invoked
- * @param seqActivationId the id of the sequence
- * @param inputPayload the payload passed to the first component in the sequence
- * @param components the components in the sequence
- * @param cause the activation id of the sequence that lead to invoking this sequence or None if this sequence is topmost
- * @param atomicActionCnt the dynamic atomic action count observed so far since the start of the execution of the topmost sequence
- * @return a future which resolves with the accounting for a sequence, including the last result, duration, and activation ids
- */
- private def invokeSequenceComponents(
- user: Identity,
- seqAction: WhiskAction,
- seqActivationId: ActivationId,
- inputPayload: Option[JsObject],
- components: Vector[FullyQualifiedEntityName],
- cause: Option[ActivationId],
- atomicActionCnt: Int)(
- implicit transid: TransactionId): Future[SequenceAccounting] = {
-
- // For each action in the sequence, fetch any of its associated parameters (including package or binding).
- // We do this for all of the actions in the sequence even though it may be short circuited. This is to
- // hide the latency of the fetches from the datastore and the parameter merging that has to occur. It
- // may be desirable in the future to selectively speculate over a smaller number of components rather than
- // the entire sequence.
- //
- // This action/parameter resolution is done in futures; the execution starts as soon as the first component
- // is resolved.
- val resolvedFutureActions = resolveDefaultNamespace(components, user) map {
- c => WhiskAction.resolveActionAndMergeParameters(entityStore, c)
- }
-
- // this holds the initial value of the accounting structure, including the input boxed as an ActivationResponse
- val initialAccounting = Future.successful {
- SequenceAccounting(atomicActionCnt, ActivationResponse.payloadPlaceholder(inputPayload))
- }
+ // create the whisk activation
+ WhiskActivation(
+ namespace = user.namespace.toPath,
+ name = action.name,
+ user.subject,
+ activationId = activationId,
+ start = start,
+ end = end,
+ cause = if (topmost) None else cause, // propagate the cause for inner sequences, but undefined for topmost
+ response = accounting.previousResponse.getAndSet(null), // getAndSet(null) drops reference to the activation result
+ logs = accounting.finalLogs,
+ version = action.version,
+ publish = false,
+ annotations = Parameters("topmost", JsBoolean(topmost)) ++
+ Parameters("path", action.fullyQualifiedName(false).toString) ++
+ Parameters("kind", "sequence") ++
+ causedBy ++
+ sequenceLimits,
+ duration = Some(accounting.duration))
+ }
+
+ /**
+ * Invokes the components of a sequence in a blocking fashion.
+ * Returns a vector of successful futures containing the results of the invocation of all components in the sequence.
+ * Unexpected behavior is modeled through an Either with activation(right) or activation response in case of error (left).
+ *
+ * Keeps track of the dynamic atomic action count.
+ * @param user the user invoking the sequence
+ * @param seqAction the sequence invoked
+ * @param seqActivationId the id of the sequence
+ * @param inputPayload the payload passed to the first component in the sequence
+ * @param components the components in the sequence
+ * @param cause the activation id of the sequence that lead to invoking this sequence or None if this sequence is topmost
+ * @param atomicActionCnt the dynamic atomic action count observed so far since the start of the execution of the topmost sequence
+ * @return a future which resolves with the accounting for a sequence, including the last result, duration, and activation ids
+ */
+ private def invokeSequenceComponents(
+ user: Identity,
+ seqAction: WhiskAction,
+ seqActivationId: ActivationId,
+ inputPayload: Option[JsObject],
+ components: Vector[FullyQualifiedEntityName],
+ cause: Option[ActivationId],
+ atomicActionCnt: Int)(implicit transid: TransactionId): Future[SequenceAccounting] = {
+
+ // For each action in the sequence, fetch any of its associated parameters (including package or binding).
+ // We do this for all of the actions in the sequence even though it may be short circuited. This is to
+ // hide the latency of the fetches from the datastore and the parameter merging that has to occur. It
+ // may be desirable in the future to selectively speculate over a smaller number of components rather than
+ // the entire sequence.
+ //
+ // This action/parameter resolution is done in futures; the execution starts as soon as the first component
+ // is resolved.
+ val resolvedFutureActions = resolveDefaultNamespace(components, user) map { c =>
+ WhiskAction.resolveActionAndMergeParameters(entityStore, c)
+ }
- // execute the actions in sequential blocking fashion
- resolvedFutureActions.foldLeft(initialAccounting) {
- (accountingFuture, futureAction) =>
- accountingFuture.flatMap { accounting =>
- if (accounting.atomicActionCnt < actionSequenceLimit) {
- invokeNextAction(user, futureAction, accounting, cause).flatMap { accounting =>
- if (!accounting.shortcircuit) {
- Future.successful(accounting)
- } else {
- // this is to short circuit the fold
- Future.failed(FailedSequenceActivation(accounting)) // terminates the fold
- }
- }
- } else {
- val updatedAccount = accounting.fail(ActivationResponse.applicationError(sequenceIsTooLong), None)
- Future.failed(FailedSequenceActivation(updatedAccount)) // terminates the fold
- }
- }
- }.recoverWith {
- // turn the failed accounting back to success; this is the only possible failure
- // since all throwables are recovered with a failed accounting instance and this is
- // in turned boxed to FailedSequenceActivation
- case FailedSequenceActivation(accounting) => Future.successful(accounting)
- }
+ // this holds the initial value of the accounting structure, including the input boxed as an ActivationResponse
+ val initialAccounting = Future.successful {
+ SequenceAccounting(atomicActionCnt, ActivationResponse.payloadPlaceholder(inputPayload))
}
- /**
- * Invokes one component from a sequence action. Unless an unexpected whisk failure happens, the future returned is always successful.
- * The return is a tuple of
- * 1. either an activation (right) or an activation response (left) in case the activation could not be retrieved
- * 2. the dynamic count of atomic actions observed so far since the start of the topmost sequence on behalf which this action is executing
- *
- * The method distinguishes between invoking a sequence or an atomic action.
- * @param user the user executing the sequence
- * @param futureAction the future which fetches the action to be invoked from the db
- * @param accounting the state of the sequence activation, contains the dynamic activation count, logs and payload for the next action
- * @param cause the activation id of the first sequence containing this activations
- * @return a future which resolves with updated accounting for a sequence, including the last result, duration, and activation ids
- */
- private def invokeNextAction(
- user: Identity,
- futureAction: Future[WhiskAction],
- accounting: SequenceAccounting,
- cause: Option[ActivationId])(
- implicit transid: TransactionId): Future[SequenceAccounting] = {
- futureAction.flatMap { action =>
- // the previous response becomes input for the next action in the sequence;
- // the accounting no longer needs to hold a reference to it once the action is
- // invoked, so previousResponse.getAndSet(null) drops the reference at this point
- // which prevents dragging the previous response for the lifetime of the next activation
- val inputPayload = accounting.previousResponse.getAndSet(null).result.map(_.asJsObject)
-
- // invoke the action by calling the right method depending on whether it's an atomic action or a sequence
- val futureWhiskActivationTuple = action.toExecutableWhiskAction match {
- case None =>
- val SequenceExec(components) = action.exec
- logging.info(this, s"sequence invoking an enclosed sequence $action")
- // call invokeSequence to invoke the inner sequence; this is a blocking activation by definition
- invokeSequence(user, action, components, inputPayload, None, cause, topmost = false, accounting.atomicActionCnt)
- case Some(executable) =>
- // this is an invoke for an atomic action
- logging.info(this, s"sequence invoking an enclosed atomic action $action")
- val timeout = action.limits.timeout.duration + 1.minute
- invokeAction(user, action, inputPayload, waitForResponse = Some(timeout), cause) map {
- case res => (res, accounting.atomicActionCnt + 1)
- }
+ // execute the actions in sequential blocking fashion
+ resolvedFutureActions
+ .foldLeft(initialAccounting) { (accountingFuture, futureAction) =>
+ accountingFuture.flatMap { accounting =>
+ if (accounting.atomicActionCnt < actionSequenceLimit) {
+ invokeNextAction(user, futureAction, accounting, cause).flatMap { accounting =>
+ if (!accounting.shortcircuit) {
+ Future.successful(accounting)
+ } else {
+ // this is to short circuit the fold
+ Future.failed(FailedSequenceActivation(accounting)) // terminates the fold
+ }
}
+ } else {
+ val updatedAccount = accounting.fail(ActivationResponse.applicationError(sequenceIsTooLong), None)
+ Future.failed(FailedSequenceActivation(updatedAccount)) // terminates the fold
+ }
+ }
+ }
+ .recoverWith {
+ // turn the failed accounting back to success; this is the only possible failure
+ // since all throwables are recovered with a failed accounting instance and this is
+ // in turned boxed to FailedSequenceActivation
+ case FailedSequenceActivation(accounting) => Future.successful(accounting)
+ }
+ }
+
+ /**
+ * Invokes one component from a sequence action. Unless an unexpected whisk failure happens, the future returned is always successful.
+ * The return is a tuple of
+ * 1. either an activation (right) or an activation response (left) in case the activation could not be retrieved
+ * 2. the dynamic count of atomic actions observed so far since the start of the topmost sequence on behalf which this action is executing
+ *
+ * The method distinguishes between invoking a sequence or an atomic action.
+ * @param user the user executing the sequence
+ * @param futureAction the future which fetches the action to be invoked from the db
+ * @param accounting the state of the sequence activation, contains the dynamic activation count, logs and payload for the next action
+ * @param cause the activation id of the first sequence containing this activations
+ * @return a future which resolves with updated accounting for a sequence, including the last result, duration, and activation ids
+ */
+ private def invokeNextAction(
+ user: Identity,
+ futureAction: Future[WhiskAction],
+ accounting: SequenceAccounting,
+ cause: Option[ActivationId])(implicit transid: TransactionId): Future[SequenceAccounting] = {
+ futureAction.flatMap { action =>
+ // the previous response becomes input for the next action in the sequence;
+ // the accounting no longer needs to hold a reference to it once the action is
+ // invoked, so previousResponse.getAndSet(null) drops the reference at this point
+ // which prevents dragging the previous response for the lifetime of the next activation
+ val inputPayload = accounting.previousResponse.getAndSet(null).result.map(_.asJsObject)
+
+ // invoke the action by calling the right method depending on whether it's an atomic action or a sequence
+ val futureWhiskActivationTuple = action.toExecutableWhiskAction match {
+ case None =>
+ val SequenceExec(components) = action.exec
+ logging.info(this, s"sequence invoking an enclosed sequence $action")
+ // call invokeSequence to invoke the inner sequence; this is a blocking activation by definition
+ invokeSequence(
+ user,
+ action,
+ components,
+ inputPayload,
+ None,
+ cause,
+ topmost = false,
+ accounting.atomicActionCnt)
+ case Some(executable) =>
+ // this is an invoke for an atomic action
+ logging.info(this, s"sequence invoking an enclosed atomic action $action")
+ val timeout = action.limits.timeout.duration + 1.minute
+ invokeAction(user, action, inputPayload, waitForResponse = Some(timeout), cause) map {
+ case res => (res, accounting.atomicActionCnt + 1)
+ }
+ }
+
+ futureWhiskActivationTuple
+ .map {
+ case (Right(activation), atomicActionCountSoFar) =>
+ accounting.maybe(activation, atomicActionCountSoFar, actionSequenceLimit)
+
+ case (Left(activationId), atomicActionCountSoFar) =>
+ // the result could not be retrieved in time either from active ack or from db
+ logging.error(this, s"component activation timedout for $activationId")
+ val activationResponse = ActivationResponse.whiskError(sequenceRetrieveActivationTimeout(activationId))
+ accounting.fail(activationResponse, Some(activationId))
- futureWhiskActivationTuple.map {
- case (Right(activation), atomicActionCountSoFar) =>
- accounting.maybe(activation, atomicActionCountSoFar, actionSequenceLimit)
-
- case (Left(activationId), atomicActionCountSoFar) =>
- // the result could not be retrieved in time either from active ack or from db
- logging.error(this, s"component activation timedout for $activationId")
- val activationResponse = ActivationResponse.whiskError(sequenceRetrieveActivationTimeout(activationId))
- accounting.fail(activationResponse, Some(activationId))
-
- }.recover {
- // check any failure here and generate an activation response to encapsulate
- // the failure mode; consider this failure a whisk error
- case t: Throwable =>
- logging.error(this, s"component activation failed: $t")
- accounting.fail(ActivationResponse.whiskError(sequenceActivationFailure), None)
- }
+ }
+ .recover {
+ // check any failure here and generate an activation response to encapsulate
+ // the failure mode; consider this failure a whisk error
+ case t: Throwable =>
+ logging.error(this, s"component activation failed: $t")
+ accounting.fail(ActivationResponse.whiskError(sequenceActivationFailure), None)
}
}
+ }
- /** Replaces default namespaces in a vector of components from a sequence with appropriate namespace. */
- private def resolveDefaultNamespace(components: Vector[FullyQualifiedEntityName], user: Identity): Vector[FullyQualifiedEntityName] = {
- // resolve any namespaces that may appears as "_" (the default namespace)
- components.map(c => FullyQualifiedEntityName(c.path.resolveNamespace(user.namespace), c.name))
- }
+ /** Replaces default namespaces in a vector of components from a sequence with appropriate namespace. */
+ private def resolveDefaultNamespace(components: Vector[FullyQualifiedEntityName],
+ user: Identity): Vector[FullyQualifiedEntityName] = {
+ // resolve any namespaces that may appears as "_" (the default namespace)
+ components.map(c => FullyQualifiedEntityName(c.path.resolveNamespace(user.namespace), c.name))
+ }
- /** Max atomic action count allowed for sequences */
- private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt
+ /** Max atomic action count allowed for sequences */
+ private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt
}
/**
@@ -369,72 +394,71 @@ protected[actions] trait SequenceActions {
* components (needed to annotate the sequence with GB-s)
* @param shortcircuit when true, stops the execution of the next component in the sequence
*/
-protected[actions] case class SequenceAccounting(
- atomicActionCnt: Int,
- previousResponse: AtomicReference[ActivationResponse],
- logs: mutable.Buffer[ActivationId],
- duration: Long = 0,
- maxMemory: Option[Int] = None,
- shortcircuit: Boolean = false) {
-
- /** @return the ActivationLogs data structure for this sequence invocation */
- def finalLogs = ActivationLogs(logs.map(id => id.asString).toVector)
-
- /** The previous activation was successful. */
- private def success(activation: WhiskActivation, newCnt: Int, shortcircuit: Boolean = false) = {
- previousResponse.set(null)
- SequenceAccounting(
- prev = this,
- newCnt = newCnt,
- shortcircuit = shortcircuit,
- incrDuration = activation.duration,
- newResponse = activation.response,
- newActivationId = activation.activationId,
- newMemoryLimit = activation.annotations.get("limits") map {
- limitsAnnotation => // we have a limits annotation
- limitsAnnotation.asJsObject.getFields("memory") match {
- case Seq(JsNumber(memory)) => Some(memory.toInt) // we have a numerical "memory" field in the "limits" annotation
- }
- } getOrElse { None })
- }
-
- /** The previous activation failed (this is used when there is no activation record or an internal error. */
- def fail(failureResponse: ActivationResponse, activationId: Option[ActivationId]) = {
- require(!failureResponse.isSuccess)
- logs.appendAll(activationId)
- copy(previousResponse = new AtomicReference(failureResponse), shortcircuit = true)
- }
-
- /** Determines whether the previous activation succeeded or failed. */
- def maybe(activation: WhiskActivation, newCnt: Int, maxSequenceCnt: Int) = {
- // check conditions on payload that may lead to interrupting the execution of the sequence
- // short-circuit the execution of the sequence iff the payload contains an error field
- // and is the result of an action return, not the initial payload
- val outputPayload = activation.response.result.map(_.asJsObject)
- val payloadContent = outputPayload getOrElse JsObject.empty
- val errorField = payloadContent.fields.get(ActivationResponse.ERROR_FIELD)
- val withinSeqLimit = newCnt <= maxSequenceCnt
-
- if (withinSeqLimit && errorField.isEmpty) {
- // all good with this action invocation
- success(activation, newCnt)
- } else {
- val nextActivation = if (!withinSeqLimit) {
- // no error in the activation but the dynamic count of actions exceeds the threshold
- // this is here as defensive code; the activation should not occur if its takes the
- // count above its limit
- val newResponse = ActivationResponse.applicationError(sequenceIsTooLong)
- activation.copy(response = newResponse)
- } else {
- assert(errorField.isDefined)
- activation
- }
-
- // there is an error field in the activation response. here, we treat this like success,
- // in the sense of tallying up the accounting fields, but terminate the sequence early
- success(nextActivation, newCnt, shortcircuit = true)
+protected[actions] case class SequenceAccounting(atomicActionCnt: Int,
+ previousResponse: AtomicReference[ActivationResponse],
+ logs: mutable.Buffer[ActivationId],
+ duration: Long = 0,
+ maxMemory: Option[Int] = None,
+ shortcircuit: Boolean = false) {
+
+ /** @return the ActivationLogs data structure for this sequence invocation */
+ def finalLogs = ActivationLogs(logs.map(id => id.asString).toVector)
+
+ /** The previous activation was successful. */
+ private def success(activation: WhiskActivation, newCnt: Int, shortcircuit: Boolean = false) = {
+ previousResponse.set(null)
+ SequenceAccounting(
+ prev = this,
+ newCnt = newCnt,
+ shortcircuit = shortcircuit,
+ incrDuration = activation.duration,
+ newResponse = activation.response,
+ newActivationId = activation.activationId,
+ newMemoryLimit = activation.annotations.get("limits") map { limitsAnnotation => // we have a limits annotation
+ limitsAnnotation.asJsObject.getFields("memory") match {
+ case Seq(JsNumber(memory)) =>
+ Some(memory.toInt) // we have a numerical "memory" field in the "limits" annotation
}
+ } getOrElse { None })
+ }
+
+ /** The previous activation failed (this is used when there is no activation record or an internal error. */
+ def fail(failureResponse: ActivationResponse, activationId: Option[ActivationId]) = {
+ require(!failureResponse.isSuccess)
+ logs.appendAll(activationId)
+ copy(previousResponse = new AtomicReference(failureResponse), shortcircuit = true)
+ }
+
+ /** Determines whether the previous activation succeeded or failed. */
+ def maybe(activation: WhiskActivation, newCnt: Int, maxSequenceCnt: Int) = {
+ // check conditions on payload that may lead to interrupting the execution of the sequence
+ // short-circuit the execution of the sequence iff the payload contains an error field
+ // and is the result of an action return, not the initial payload
+ val outputPayload = activation.response.result.map(_.asJsObject)
+ val payloadContent = outputPayload getOrElse JsObject.empty
+ val errorField = payloadContent.fields.get(ActivationResponse.ERROR_FIELD)
+ val withinSeqLimit = newCnt <= maxSequenceCnt
+
+ if (withinSeqLimit && errorField.isEmpty) {
+ // all good with this action invocation
+ success(activation, newCnt)
+ } else {
+ val nextActivation = if (!withinSeqLimit) {
+ // no error in the activation but the dynamic count of actions exceeds the threshold
+ // this is here as defensive code; the activation should not occur if its takes the
+ // count above its limit
+ val newResponse = ActivationResponse.applicationError(sequenceIsTooLong)
+ activation.copy(response = newResponse)
+ } else {
+ assert(errorField.isDefined)
+ activation
+ }
+
+ // there is an error field in the activation response. here, we treat this like success,
+ // in the sense of tallying up the accounting fields, but terminate the sequence early
+ success(nextActivation, newCnt, shortcircuit = true)
}
+ }
}
/**
@@ -445,39 +469,38 @@ protected[actions] case class SequenceAccounting(
*/
protected[actions] object SequenceAccounting {
- def maxMemory(prevMemoryLimit: Option[Int], newMemoryLimit: Option[Int]): Option[Int] = {
- (prevMemoryLimit ++ newMemoryLimit).reduceOption(Math.max)
- }
-
- // constructor for successful invocations, or error'ing ones (where shortcircuit = true)
- def apply(
- prev: SequenceAccounting,
- newCnt: Int,
- incrDuration: Option[Long],
- newResponse: ActivationResponse,
- newActivationId: ActivationId,
- newMemoryLimit: Option[Int],
- shortcircuit: Boolean): SequenceAccounting = {
-
- // compute the new max memory
- val newMaxMemory = maxMemory(prev.maxMemory, newMemoryLimit)
-
- // append log entry
- prev.logs += newActivationId
-
- SequenceAccounting(
- atomicActionCnt = newCnt,
- previousResponse = new AtomicReference(newResponse),
- logs = prev.logs,
- duration = incrDuration map { prev.duration + _ } getOrElse { prev.duration },
- maxMemory = newMaxMemory,
- shortcircuit = shortcircuit)
- }
-
- // constructor for initial payload
- def apply(atomicActionCnt: Int, initialPayload: ActivationResponse): SequenceAccounting = {
- SequenceAccounting(atomicActionCnt, new AtomicReference(initialPayload), mutable.Buffer.empty)
- }
+ def maxMemory(prevMemoryLimit: Option[Int], newMemoryLimit: Option[Int]): Option[Int] = {
+ (prevMemoryLimit ++ newMemoryLimit).reduceOption(Math.max)
+ }
+
+ // constructor for successful invocations, or error'ing ones (where shortcircuit = true)
+ def apply(prev: SequenceAccounting,
+ newCnt: Int,
+ incrDuration: Option[Long],
+ newResponse: ActivationResponse,
+ newActivationId: ActivationId,
+ newMemoryLimit: Option[Int],
+ shortcircuit: Boolean): SequenceAccounting = {
+
+ // compute the new max memory
+ val newMaxMemory = maxMemory(prev.maxMemory, newMemoryLimit)
+
+ // append log entry
+ prev.logs += newActivationId
+
+ SequenceAccounting(
+ atomicActionCnt = newCnt,
+ previousResponse = new AtomicReference(newResponse),
+ logs = prev.logs,
+ duration = incrDuration map { prev.duration + _ } getOrElse { prev.duration },
+ maxMemory = newMaxMemory,
+ shortcircuit = shortcircuit)
+ }
+
+ // constructor for initial payload
+ def apply(atomicActionCnt: Int, initialPayload: ActivationResponse): SequenceAccounting = {
+ SequenceAccounting(atomicActionCnt, new AtomicReference(initialPayload), mutable.Buffer.empty)
+ }
}
protected[actions] case class FailedSequenceActivation(accounting: SequenceAccounting) extends Throwable
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/ActionCollection.scala b/core/controller/src/main/scala/whisk/core/entitlement/ActionCollection.scala
index 1691f4a..b4ab0c9 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/ActionCollection.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/ActionCollection.scala
@@ -28,29 +28,33 @@ import whisk.core.entity.types.EntityStore
class ActionCollection(entityStore: EntityStore) extends Collection(Collection.ACTIONS) {
- protected override val allowedEntityRights = Privilege.ALL
-
- /**
- * Computes implicit rights on an action (sequence, in package, or primitive).
- */
- protected[core] override def implicitRights(user: Identity, namespaces: Set[String], right: Privilege, resource: Resource)(
- implicit ep: EntitlementProvider, ec: ExecutionContext, transid: TransactionId) = {
- val isOwner = namespaces.contains(resource.namespace.root.asString)
- resource.entity map {
- name =>
- right match {
- // if action is in a package, check that the user is entitled to package [binding]
- case (Privilege.READ | Privilege.ACTIVATE) if !resource.namespace.defaultPackage =>
- val packageNamespace = resource.namespace.root.toPath
- val packageName = Some(resource.namespace.last.name)
- val packageResource = Resource(packageNamespace, Collection(Collection.PACKAGES), packageName)
- ep.check(user, Privilege.READ, packageResource) map { _ => true }
- case _ => Future.successful(isOwner && allowedEntityRights.contains(right))
- }
- } getOrElse {
- // only a READ on the action collection is permitted if this is the owner of the collection
- Future.successful(isOwner && right == Privilege.READ)
- }
+ protected override val allowedEntityRights = Privilege.ALL
+
+ /**
+ * Computes implicit rights on an action (sequence, in package, or primitive).
+ */
+ protected[core] override def implicitRights(
+ user: Identity,
+ namespaces: Set[String],
+ right: Privilege,
+ resource: Resource)(implicit ep: EntitlementProvider, ec: ExecutionContext, transid: TransactionId) = {
+ val isOwner = namespaces.contains(resource.namespace.root.asString)
+ resource.entity map { name =>
+ right match {
+ // if action is in a package, check that the user is entitled to package [binding]
+ case (Privilege.READ | Privilege.ACTIVATE) if !resource.namespace.defaultPackage =>
+ val packageNamespace = resource.namespace.root.toPath
+ val packageName = Some(resource.namespace.last.name)
+ val packageResource = Resource(packageNamespace, Collection(Collection.PACKAGES), packageName)
+ ep.check(user, Privilege.READ, packageResource) map { _ =>
+ true
+ }
+ case _ => Future.successful(isOwner && allowedEntityRights.contains(right))
+ }
+ } getOrElse {
+ // only a READ on the action collection is permitted if this is the owner of the collection
+ Future.successful(isOwner && right == Privilege.READ)
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/ActivationThrottler.scala b/core/controller/src/main/scala/whisk/core/entitlement/ActivationThrottler.scala
index b184d50..688d807 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/ActivationThrottler.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/ActivationThrottler.scala
@@ -32,26 +32,30 @@ import whisk.core.loadBalancer.LoadBalancer
* @param defaultConcurrencyLimit the default max allowed concurrent operations
* @param systemOverloadLimit the limit when the system is considered overloaded
*/
-class ActivationThrottler(loadBalancer: LoadBalancer, defaultConcurrencyLimit: Int, systemOverloadLimit: Int)(implicit logging: Logging) {
+class ActivationThrottler(loadBalancer: LoadBalancer, defaultConcurrencyLimit: Int, systemOverloadLimit: Int)(
+ implicit logging: Logging) {
- logging.info(this, s"concurrencyLimit = $defaultConcurrencyLimit, systemOverloadLimit = $systemOverloadLimit")(TransactionId.controller)
+ logging.info(this, s"concurrencyLimit = $defaultConcurrencyLimit, systemOverloadLimit = $systemOverloadLimit")(
+ TransactionId.controller)
- /**
- * Checks whether the operation should be allowed to proceed.
- */
- def check(user: Identity)(implicit tid: TransactionId): Boolean = {
- val concurrentActivations = loadBalancer.activeActivationsFor(user.uuid)
- val concurrencyLimit = user.limits.concurrentInvocations.getOrElse(defaultConcurrencyLimit)
- logging.info(this, s"namespace = ${user.uuid.asString}, concurrent activations = $concurrentActivations, below limit = $concurrencyLimit")
- concurrentActivations < concurrencyLimit
- }
+ /**
+ * Checks whether the operation should be allowed to proceed.
+ */
+ def check(user: Identity)(implicit tid: TransactionId): Boolean = {
+ val concurrentActivations = loadBalancer.activeActivationsFor(user.uuid)
+ val concurrencyLimit = user.limits.concurrentInvocations.getOrElse(defaultConcurrencyLimit)
+ logging.info(
+ this,
+ s"namespace = ${user.uuid.asString}, concurrent activations = $concurrentActivations, below limit = $concurrencyLimit")
+ concurrentActivations < concurrencyLimit
+ }
- /**
- * Checks whether the system is in a generally overloaded state.
- */
- def isOverloaded()(implicit tid: TransactionId): Boolean = {
- val concurrentActivations = loadBalancer.totalActiveActivations
- logging.info(this, s"concurrent activations in system = $concurrentActivations, below limit = $systemOverloadLimit")
- concurrentActivations > systemOverloadLimit
- }
+ /**
+ * Checks whether the system is in a generally overloaded state.
+ */
+ def isOverloaded()(implicit tid: TransactionId): Boolean = {
+ val concurrentActivations = loadBalancer.totalActiveActivations
+ logging.info(this, s"concurrent activations in system = $concurrentActivations, below limit = $systemOverloadLimit")
+ concurrentActivations > systemOverloadLimit
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/Collection.scala b/core/controller/src/main/scala/whisk/core/entitlement/Collection.scala
index be406a6..bfe0200 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/Collection.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/Collection.scala
@@ -47,95 +47,106 @@ import whisk.core.entity.types.EntityStore
* @param activate the privilege for an activate (may be ACTIVATE or REJECT for example)
* @param listLimit the default limit on number of entities returned from a collection on a list operation
*/
-protected[core] case class Collection protected (
- val path: String,
- val listLimit: Int = 30) {
- override def toString = path
-
- /** Determines the right to request for the resources and context. */
- protected[core] def determineRight(op: HttpMethod, resource: Option[String])(
- implicit transid: TransactionId): Privilege = {
- op match {
- case GET => Privilege.READ
- case PUT => resource map { _ => Privilege.PUT } getOrElse Privilege.REJECT
- case POST => resource map { _ => activateAllowed } getOrElse Privilege.REJECT
- case DELETE => resource map { _ => Privilege.DELETE } getOrElse Privilege.REJECT
- case _ => Privilege.REJECT
- }
- }
-
- protected val allowedCollectionRights = Set(Privilege.READ)
- protected val allowedEntityRights = {
- Set(Privilege.READ, Privilege.PUT, Privilege.ACTIVATE, Privilege.DELETE)
- }
-
- private lazy val activateAllowed = {
- if (allowedEntityRights.contains(Privilege.ACTIVATE)) {
- Privilege.ACTIVATE
- } else Privilege.REJECT
+protected[core] case class Collection protected (val path: String, val listLimit: Int = 30) {
+ override def toString = path
+
+ /** Determines the right to request for the resources and context. */
+ protected[core] def determineRight(op: HttpMethod, resource: Option[String])(
+ implicit transid: TransactionId): Privilege = {
+ op match {
+ case GET => Privilege.READ
+ case PUT =>
+ resource map { _ =>
+ Privilege.PUT
+ } getOrElse Privilege.REJECT
+ case POST =>
+ resource map { _ =>
+ activateAllowed
+ } getOrElse Privilege.REJECT
+ case DELETE =>
+ resource map { _ =>
+ Privilege.DELETE
+ } getOrElse Privilege.REJECT
+ case _ => Privilege.REJECT
}
-
- /**
- * Infers implicit rights on a resource in the collection before checking explicit
- * rights in the entitlement matrix. The subject has CRUD and activate rights
- * to any resource in in any of their namespaces as long as the right (the implied operation)
- * is permitted on the resource.
- */
- protected[core] def implicitRights(user: Identity, namespaces: Set[String], right: Privilege, resource: Resource)(
- implicit ep: EntitlementProvider, ec: ExecutionContext, transid: TransactionId): Future[Boolean] = Future.successful {
- // if the resource root namespace is in any of the allowed namespaces
- // then this is an owner of the resource
- val self = namespaces.contains(resource.namespace.root.asString)
-
- resource.entity map {
- _ => self && allowedEntityRights.contains(right)
- } getOrElse {
- self && allowedCollectionRights.contains(right)
- }
+ }
+
+ protected val allowedCollectionRights = Set(Privilege.READ)
+ protected val allowedEntityRights = {
+ Set(Privilege.READ, Privilege.PUT, Privilege.ACTIVATE, Privilege.DELETE)
+ }
+
+ private lazy val activateAllowed = {
+ if (allowedEntityRights.contains(Privilege.ACTIVATE)) {
+ Privilege.ACTIVATE
+ } else Privilege.REJECT
+ }
+
+ /**
+ * Infers implicit rights on a resource in the collection before checking explicit
+ * rights in the entitlement matrix. The subject has CRUD and activate rights
+ * to any resource in in any of their namespaces as long as the right (the implied operation)
+ * is permitted on the resource.
+ */
+ protected[core] def implicitRights(user: Identity, namespaces: Set[String], right: Privilege, resource: Resource)(
+ implicit ep: EntitlementProvider,
+ ec: ExecutionContext,
+ transid: TransactionId): Future[Boolean] = Future.successful {
+ // if the resource root namespace is in any of the allowed namespaces
+ // then this is an owner of the resource
+ val self = namespaces.contains(resource.namespace.root.asString)
+
+ resource.entity map { _ =>
+ self && allowedEntityRights.contains(right)
+ } getOrElse {
+ self && allowedCollectionRights.contains(right)
}
+ }
}
/** An enumeration of known collections. */
protected[core] object Collection {
- protected[core] def requiredProperties = WhiskEntityStore.requiredProperties
-
- protected[core] val ACTIONS = WhiskAction.collectionName
- protected[core] val TRIGGERS = WhiskTrigger.collectionName
- protected[core] val RULES = WhiskRule.collectionName
- protected[core] val PACKAGES = WhiskPackage.collectionName
- protected[core] val ACTIVATIONS = WhiskActivation.collectionName
- protected[core] val NAMESPACES = "namespaces"
-
- private val collections = scala.collection.mutable.Map[String, Collection]()
- private def register(c: Collection) = collections += c.path -> c
-
- protected[core] def apply(name: String) = collections.get(name).get
-
- protected[core] def initialize(entityStore: EntityStore)(implicit logging: Logging) = {
- register(new ActionCollection(entityStore))
- register(new Collection(TRIGGERS))
- register(new Collection(RULES))
- register(new PackageCollection(entityStore))
-
- register(new Collection(ACTIVATIONS) {
- protected[core] override def determineRight(op: HttpMethod, resource: Option[String])(
- implicit transid: TransactionId) = {
- if (op == GET) Privilege.READ else Privilege.REJECT
- }
-
- protected override val allowedEntityRights = Set(Privilege.READ)
- })
-
- register(new Collection(NAMESPACES) {
- protected[core] override def determineRight(op: HttpMethod, resource: Option[String])(
- implicit transid: TransactionId) = {
- resource map { _ => Privilege.REJECT } getOrElse {
- if (op == GET) Privilege.READ else Privilege.REJECT
- }
- }
-
- protected override val allowedEntityRights = Set(Privilege.READ)
- })
- }
+ protected[core] def requiredProperties = WhiskEntityStore.requiredProperties
+
+ protected[core] val ACTIONS = WhiskAction.collectionName
+ protected[core] val TRIGGERS = WhiskTrigger.collectionName
+ protected[core] val RULES = WhiskRule.collectionName
+ protected[core] val PACKAGES = WhiskPackage.collectionName
+ protected[core] val ACTIVATIONS = WhiskActivation.collectionName
+ protected[core] val NAMESPACES = "namespaces"
+
+ private val collections = scala.collection.mutable.Map[String, Collection]()
+ private def register(c: Collection) = collections += c.path -> c
+
+ protected[core] def apply(name: String) = collections.get(name).get
+
+ protected[core] def initialize(entityStore: EntityStore)(implicit logging: Logging) = {
+ register(new ActionCollection(entityStore))
+ register(new Collection(TRIGGERS))
+ register(new Collection(RULES))
+ register(new PackageCollection(entityStore))
+
+ register(new Collection(ACTIVATIONS) {
+ protected[core] override def determineRight(op: HttpMethod,
+ resource: Option[String])(implicit transid: TransactionId) = {
+ if (op == GET) Privilege.READ else Privilege.REJECT
+ }
+
+ protected override val allowedEntityRights = Set(Privilege.READ)
+ })
+
+ register(new Collection(NAMESPACES) {
+ protected[core] override def determineRight(op: HttpMethod,
+ resource: Option[String])(implicit transid: TransactionId) = {
+ resource map { _ =>
+ Privilege.REJECT
+ } getOrElse {
+ if (op == GET) Privilege.READ else Privilege.REJECT
+ }
+ }
+
+ protected override val allowedEntityRights = Set(Privilege.READ)
+ })
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala b/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
index 0bf2abb..5e04366 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
@@ -38,7 +38,7 @@ import whisk.core.loadBalancer.LoadBalancer
import whisk.http.Messages._
package object types {
- type Entitlements = TrieMap[(Subject, String), Set[Privilege]]
+ type Entitlements = TrieMap[(Subject, String), Set[Privilege]]
}
/**
@@ -50,23 +50,22 @@ package object types {
* @param entity an optional entity name that identifies a specific item in the collection
* @param env an optional environment to bind to the resource during an activation
*/
-protected[core] case class Resource(
- namespace: EntityPath,
- collection: Collection,
- entity: Option[String],
- env: Option[Parameters] = None) {
- def parent = collection.path + EntityPath.PATHSEP + namespace
- def id = parent + (entity map { EntityPath.PATHSEP + _ } getOrElse (""))
- override def toString = id
+protected[core] case class Resource(namespace: EntityPath,
+ collection: Collection,
+ entity: Option[String],
+ env: Option[Parameters] = None) {
+ def parent = collection.path + EntityPath.PATHSEP + namespace
+ def id = parent + (entity map { EntityPath.PATHSEP + _ } getOrElse (""))
+ override def toString = id
}
protected[core] object EntitlementProvider {
- val requiredProperties = Map(
- WhiskConfig.actionInvokePerMinuteLimit -> null,
- WhiskConfig.actionInvokeConcurrentLimit -> null,
- WhiskConfig.triggerFirePerMinuteLimit -> null,
- WhiskConfig.actionInvokeSystemOverloadLimit -> null)
+ val requiredProperties = Map(
+ WhiskConfig.actionInvokePerMinuteLimit -> null,
+ WhiskConfig.actionInvokeConcurrentLimit -> null,
+ WhiskConfig.triggerFirePerMinuteLimit -> null,
+ WhiskConfig.actionInvokeSystemOverloadLimit -> null)
}
/**
@@ -74,217 +73,229 @@ protected[core] object EntitlementProvider {
* This is where enforcement of activation quotas takes place, in additional to basic authorization.
*/
protected[core] abstract class EntitlementProvider(config: WhiskConfig, loadBalancer: LoadBalancer)(
- implicit actorSystem: ActorSystem, logging: Logging) {
+ implicit actorSystem: ActorSystem,
+ logging: Logging) {
- private implicit val executionContext = actorSystem.dispatcher
+ private implicit val executionContext = actorSystem.dispatcher
- private val invokeRateThrottler = new RateThrottler("actions per minute", config.actionInvokePerMinuteLimit.toInt, _.limits.invocationsPerMinute)
- private val triggerRateThrottler = new RateThrottler("triggers per minute", config.triggerFirePerMinuteLimit.toInt, _.limits.firesPerMinute)
- private val concurrentInvokeThrottler = new ActivationThrottler(loadBalancer, config.actionInvokeConcurrentLimit.toInt, config.actionInvokeSystemOverloadLimit.toInt)
+ private val invokeRateThrottler =
+ new RateThrottler("actions per minute", config.actionInvokePerMinuteLimit.toInt, _.limits.invocationsPerMinute)
+ private val triggerRateThrottler =
+ new RateThrottler("triggers per minute", config.triggerFirePerMinuteLimit.toInt, _.limits.firesPerMinute)
+ private val concurrentInvokeThrottler = new ActivationThrottler(
+ loadBalancer,
+ config.actionInvokeConcurrentLimit.toInt,
+ config.actionInvokeSystemOverloadLimit.toInt)
- /**
- * Grants a subject the right to access a resources.
- *
- * @param subject the subject to grant right to
- * @param right the privilege to grant the subject
- * @param resource the resource to grant the subject access to
- * @return a promise that completes with true iff the subject is granted the right to access the requested resource
- */
- protected[core] def grant(subject: Subject, right: Privilege, resource: Resource)(implicit transid: TransactionId): Future[Boolean]
+ /**
+ * Grants a subject the right to access a resources.
+ *
+ * @param subject the subject to grant right to
+ * @param right the privilege to grant the subject
+ * @param resource the resource to grant the subject access to
+ * @return a promise that completes with true iff the subject is granted the right to access the requested resource
+ */
+ protected[core] def grant(subject: Subject, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId): Future[Boolean]
- /**
- * Revokes a subject the right to access a resources.
- *
- * @param subject the subject to revoke right to
- * @param right the privilege to revoke the subject
- * @param resource the resource to revoke the subject access to
- * @return a promise that completes with true iff the subject is revoked the right to access the requested resource
- */
- protected[core] def revoke(subject: Subject, right: Privilege, resource: Resource)(implicit transid: TransactionId): Future[Boolean]
+ /**
+ * Revokes a subject the right to access a resources.
+ *
+ * @param subject the subject to revoke right to
+ * @param right the privilege to revoke the subject
+ * @param resource the resource to revoke the subject access to
+ * @return a promise that completes with true iff the subject is revoked the right to access the requested resource
+ */
+ protected[core] def revoke(subject: Subject, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId): Future[Boolean]
- /**
- * Checks if a subject is entitled to a resource because it was granted the right explicitly.
- *
- * @param subject the subject to check rights for
- * @param right the privilege the subject is requesting
- * @param resource the resource the subject requests access to
- * @return a promise that completes with true iff the subject is permitted to access the request resource
- */
- protected def entitled(subject: Subject, right: Privilege, resource: Resource)(implicit transid: TransactionId): Future[Boolean]
+ /**
+ * Checks if a subject is entitled to a resource because it was granted the right explicitly.
+ *
+ * @param subject the subject to check rights for
+ * @param right the privilege the subject is requesting
+ * @param resource the resource the subject requests access to
+ * @return a promise that completes with true iff the subject is permitted to access the request resource
+ */
+ protected def entitled(subject: Subject, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId): Future[Boolean]
- /**
- * Checks action activation rate throttles for an identity.
- *
- * @param user the identity to check rate throttles for
- * @return a promise that completes with success iff the user is within their activation quota
- */
- protected[core] def checkThrottles(user: Identity)(
- implicit transid: TransactionId): Future[Unit] = {
+ /**
+ * Checks action activation rate throttles for an identity.
+ *
+ * @param user the identity to check rate throttles for
+ * @return a promise that completes with success iff the user is within their activation quota
+ */
+ protected[core] def checkThrottles(user: Identity)(implicit transid: TransactionId): Future[Unit] = {
- logging.info(this, s"checking user '${user.subject}' has not exceeded activation quota")
+ logging.info(this, s"checking user '${user.subject}' has not exceeded activation quota")
- checkSystemOverload(ACTIVATE) orElse {
- checkThrottleOverload(!invokeRateThrottler.check(user), tooManyRequests)
- } orElse {
- checkThrottleOverload(!concurrentInvokeThrottler.check(user), tooManyConcurrentRequests)
- } map {
- Future.failed(_)
- } getOrElse Future.successful({})
- }
-
- /**
- * Checks if a subject has the right to access a specific resource. The entitlement may be implicit,
- * that is, inferred based on namespaces that a subject belongs to and the namespace of the
- * resource for example, or explicit. The implicit check is computed here. The explicit check
- * is delegated to the service implementing this interface.
- *
- * NOTE: do not use this method to check a package binding because this method does not allow
- * for a continuation to check that both the binding and the references package are both either
- * implicitly or explicitly granted. Instead, resolve the package binding first and use the alternate
- * method which authorizes a set of resources.
- *
- * @param user the subject to check rights for
- * @param right the privilege the subject is requesting (applies to the entire set of resources)
- * @param resource the resource the subject requests access to
- * @return a promise that completes with success iff the subject is permitted to access the requested resource
- */
- protected[core] def check(user: Identity, right: Privilege, resource: Resource)(
- implicit transid: TransactionId): Future[Unit] = check(user, right, Set(resource))
+ checkSystemOverload(ACTIVATE) orElse {
+ checkThrottleOverload(!invokeRateThrottler.check(user), tooManyRequests)
+ } orElse {
+ checkThrottleOverload(!concurrentInvokeThrottler.check(user), tooManyConcurrentRequests)
+ } map {
+ Future.failed(_)
+ } getOrElse Future.successful({})
+ }
- /**
- * Checks if a subject has the right to access a set of resources. The entitlement may be implicit,
- * that is, inferred based on namespaces that a subject belongs to and the namespace of the
- * resource for example, or explicit. The implicit check is computed here. The explicit check
- * is delegated to the service implementing this interface.
- *
- * @param user the subject identity to check rights for
- * @param right the privilege the subject is requesting (applies to the entire set of resources)
- * @param resources the set of resources the subject requests access to
- * @return a promise that completes with success iff the subject is permitted to access all of the requested resources
- */
- protected[core] def check(user: Identity, right: Privilege, resources: Set[Resource])(
- implicit transid: TransactionId): Future[Unit] = {
- val subject = user.subject
+ /**
+ * Checks if a subject has the right to access a specific resource. The entitlement may be implicit,
+ * that is, inferred based on namespaces that a subject belongs to and the namespace of the
+ * resource for example, or explicit. The implicit check is computed here. The explicit check
+ * is delegated to the service implementing this interface.
+ *
+ * NOTE: do not use this method to check a package binding because this method does not allow
+ * for a continuation to check that both the binding and the references package are both either
+ * implicitly or explicitly granted. Instead, resolve the package binding first and use the alternate
+ * method which authorizes a set of resources.
+ *
+ * @param user the subject to check rights for
+ * @param right the privilege the subject is requesting (applies to the entire set of resources)
+ * @param resource the resource the subject requests access to
+ * @return a promise that completes with success iff the subject is permitted to access the requested resource
+ */
+ protected[core] def check(user: Identity, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId): Future[Unit] = check(user, right, Set(resource))
- val entitlementCheck: Future[Boolean] = if (user.rights.contains(right)) {
- if (resources.nonEmpty) {
- logging.info(this, s"checking user '$subject' has privilege '$right' for '${resources.mkString(",")}'")
- checkSystemOverload(right) orElse {
- checkUserThrottle(user, right, resources)
- } orElse {
- checkConcurrentUserThrottle(user, right, resources)
- } map {
- Future.failed(_)
- } getOrElse checkPrivilege(user, right, resources)
- } else Future.successful(true)
- } else if (right != REJECT) {
- logging.info(this, s"supplied authkey for user '$subject' does not have privilege '$right' for '${resources.mkString(",")}'")
- Future.failed(RejectRequest(Forbidden))
- } else {
- Future.successful(false)
- }
+ /**
+ * Checks if a subject has the right to access a set of resources. The entitlement may be implicit,
+ * that is, inferred based on namespaces that a subject belongs to and the namespace of the
+ * resource for example, or explicit. The implicit check is computed here. The explicit check
+ * is delegated to the service implementing this interface.
+ *
+ * @param user the subject identity to check rights for
+ * @param right the privilege the subject is requesting (applies to the entire set of resources)
+ * @param resources the set of resources the subject requests access to
+ * @return a promise that completes with success iff the subject is permitted to access all of the requested resources
+ */
+ protected[core] def check(user: Identity, right: Privilege, resources: Set[Resource])(
+ implicit transid: TransactionId): Future[Unit] = {
+ val subject = user.subject
- entitlementCheck andThen {
- case Success(r) if resources.nonEmpty =>
- logging.info(this, if (r) "authorized" else "not authorized")
- case Failure(r: RejectRequest) =>
- logging.info(this, s"not authorized: $r")
- case Failure(t) =>
- logging.error(this, s"failed while checking entitlement: ${t.getMessage}")
- } flatMap { isAuthorized =>
- if (isAuthorized) Future.successful({})
- else Future.failed(RejectRequest(Forbidden))
- }
+ val entitlementCheck: Future[Boolean] = if (user.rights.contains(right)) {
+ if (resources.nonEmpty) {
+ logging.info(this, s"checking user '$subject' has privilege '$right' for '${resources.mkString(",")}'")
+ checkSystemOverload(right) orElse {
+ checkUserThrottle(user, right, resources)
+ } orElse {
+ checkConcurrentUserThrottle(user, right, resources)
+ } map {
+ Future.failed(_)
+ } getOrElse checkPrivilege(user, right, resources)
+ } else Future.successful(true)
+ } else if (right != REJECT) {
+ logging.info(
+ this,
+ s"supplied authkey for user '$subject' does not have privilege '$right' for '${resources.mkString(",")}'")
+ Future.failed(RejectRequest(Forbidden))
+ } else {
+ Future.successful(false)
}
- /**
- * NOTE: explicit grants do not work with package bindings because this method does not allow
- * for a continuation to check that both the binding and the references package are both either
- * implicitly or explicitly granted. Instead, the given resource set should include both the binding
- * and the referenced package.
- */
- protected def checkPrivilege(user: Identity, right: Privilege, resources: Set[Resource])(
- implicit transid: TransactionId): Future[Boolean] = {
- // check the default namespace first, bypassing additional checks if permitted
- val defaultNamespaces = Set(user.namespace.asString)
- implicit val es = this
-
- Future.sequence {
- resources.map { resource =>
- resource.collection.implicitRights(user, defaultNamespaces, right, resource) flatMap {
- case true => Future.successful(true)
- case false =>
- logging.info(this, "checking explicit grants")
- entitled(user.subject, right, resource)
- }
- }
- }.map { _.forall(identity) }
+ entitlementCheck andThen {
+ case Success(r) if resources.nonEmpty =>
+ logging.info(this, if (r) "authorized" else "not authorized")
+ case Failure(r: RejectRequest) =>
+ logging.info(this, s"not authorized: $r")
+ case Failure(t) =>
+ logging.error(this, s"failed while checking entitlement: ${t.getMessage}")
+ } flatMap { isAuthorized =>
+ if (isAuthorized) Future.successful({})
+ else Future.failed(RejectRequest(Forbidden))
}
+ }
- /**
- * Limits activations if the system is overloaded.
- *
- * @param right the privilege, if ACTIVATE then check quota else return None
- * @return None if system is not overloaded else a rejection
- */
- protected def checkSystemOverload(right: Privilege)(implicit transid: TransactionId): Option[RejectRequest] = {
- val systemOverload = right == ACTIVATE && concurrentInvokeThrottler.isOverloaded
- if (systemOverload) {
- logging.error(this, "system is overloaded")
- Some(RejectRequest(TooManyRequests, systemOverloaded))
- } else None
- }
+ /**
+ * NOTE: explicit grants do not work with package bindings because this method does not allow
+ * for a continuation to check that both the binding and the references package are both either
+ * implicitly or explicitly granted. Instead, the given resource set should include both the binding
+ * and the referenced package.
+ */
+ protected def checkPrivilege(user: Identity, right: Privilege, resources: Set[Resource])(
+ implicit transid: TransactionId): Future[Boolean] = {
+ // check the default namespace first, bypassing additional checks if permitted
+ val defaultNamespaces = Set(user.namespace.asString)
+ implicit val es = this
- /**
- * Limits activations if subject exceeds their own limits.
- * If the requested right is an activation, the set of resources must contain an activation of an action or filter to be throttled.
- * While it is possible for the set of resources to contain more than one action or trigger, the plurality is ignored and treated
- * as one activation since these should originate from a single macro resources (e.g., a sequence).
- *
- * @param user the subject identity to check rights for
- * @param right the privilege, if ACTIVATE then check quota else return None
- * @param resource the set of resources must contain at least one resource that can be activated else return None
- * @return None if subject is not throttled else a rejection
- */
- private def checkUserThrottle(user: Identity, right: Privilege, resources: Set[Resource])(
- implicit transid: TransactionId): Option[RejectRequest] = {
- def userThrottled = {
- val isInvocation = resources.exists(_.collection.path == Collection.ACTIONS)
- val isTrigger = resources.exists(_.collection.path == Collection.TRIGGERS)
- (isInvocation && !invokeRateThrottler.check(user)) || (isTrigger && !triggerRateThrottler.check(user))
+ Future
+ .sequence {
+ resources.map { resource =>
+ resource.collection.implicitRights(user, defaultNamespaces, right, resource) flatMap {
+ case true => Future.successful(true)
+ case false =>
+ logging.info(this, "checking explicit grants")
+ entitled(user.subject, right, resource)
+ }
}
+ }
+ .map { _.forall(identity) }
+ }
- checkThrottleOverload(right == ACTIVATE && userThrottled, tooManyRequests)
+ /**
+ * Limits activations if the system is overloaded.
+ *
+ * @param right the privilege, if ACTIVATE then check quota else return None
+ * @return None if system is not overloaded else a rejection
+ */
+ protected def checkSystemOverload(right: Privilege)(implicit transid: TransactionId): Option[RejectRequest] = {
+ val systemOverload = right == ACTIVATE && concurrentInvokeThrottler.isOverloaded
+ if (systemOverload) {
+ logging.error(this, "system is overloaded")
+ Some(RejectRequest(TooManyRequests, systemOverloaded))
+ } else None
+ }
+
+ /**
+ * Limits activations if subject exceeds their own limits.
+ * If the requested right is an activation, the set of resources must contain an activation of an action or filter to be throttled.
+ * While it is possible for the set of resources to contain more than one action or trigger, the plurality is ignored and treated
+ * as one activation since these should originate from a single macro resources (e.g., a sequence).
+ *
+ * @param user the subject identity to check rights for
+ * @param right the privilege, if ACTIVATE then check quota else return None
+ * @param resource the set of resources must contain at least one resource that can be activated else return None
+ * @return None if subject is not throttled else a rejection
+ */
+ private def checkUserThrottle(user: Identity, right: Privilege, resources: Set[Resource])(
+ implicit transid: TransactionId): Option[RejectRequest] = {
+ def userThrottled = {
+ val isInvocation = resources.exists(_.collection.path == Collection.ACTIONS)
+ val isTrigger = resources.exists(_.collection.path == Collection.TRIGGERS)
+ (isInvocation && !invokeRateThrottler.check(user)) || (isTrigger && !triggerRateThrottler.check(user))
}
- /**
- * Limits activations if subject exceeds limit of concurrent invocations.
- * If the requested right is an activation, the set of resources must contain an activation of an action to be throttled.
- * While it is possible for the set of resources to contain more than one action, the plurality is ignored and treated
- * as one activation since these should originate from a single macro resources (e.g., a sequence).
- *
- * @param user the subject identity to check rights for
- * @param right the privilege, if ACTIVATE then check quota else return None
- * @param resource the set of resources must contain at least one resource that can be activated else return None
- * @return None if subject is not throttled else a rejection
- */
- private def checkConcurrentUserThrottle(user: Identity, right: Privilege, resources: Set[Resource])(
- implicit transid: TransactionId): Option[RejectRequest] = {
- def userThrottled = {
- val isInvocation = resources.exists(_.collection.path == Collection.ACTIONS)
- (isInvocation && !concurrentInvokeThrottler.check(user))
- }
+ checkThrottleOverload(right == ACTIVATE && userThrottled, tooManyRequests)
+ }
- checkThrottleOverload(right == ACTIVATE && userThrottled, tooManyConcurrentRequests)
+ /**
+ * Limits activations if subject exceeds limit of concurrent invocations.
+ * If the requested right is an activation, the set of resources must contain an activation of an action to be throttled.
+ * While it is possible for the set of resources to contain more than one action, the plurality is ignored and treated
+ * as one activation since these should originate from a single macro resources (e.g., a sequence).
+ *
+ * @param user the subject identity to check rights for
+ * @param right the privilege, if ACTIVATE then check quota else return None
+ * @param resource the set of resources must contain at least one resource that can be activated else return None
+ * @return None if subject is not throttled else a rejection
+ */
+ private def checkConcurrentUserThrottle(user: Identity, right: Privilege, resources: Set[Resource])(
+ implicit transid: TransactionId): Option[RejectRequest] = {
+ def userThrottled = {
+ val isInvocation = resources.exists(_.collection.path == Collection.ACTIONS)
+ (isInvocation && !concurrentInvokeThrottler.check(user))
}
- /** Helper. */
- private def checkThrottleOverload(hasTooMany: Boolean, message: String)(
- implicit transid: TransactionId): Option[RejectRequest] = {
- if (hasTooMany) {
- Some(RejectRequest(TooManyRequests, message))
- } else None
- }
+ checkThrottleOverload(right == ACTIVATE && userThrottled, tooManyConcurrentRequests)
+ }
+
+ /** Helper. */
+ private def checkThrottleOverload(hasTooMany: Boolean, message: String)(
+ implicit transid: TransactionId): Option[RejectRequest] = {
+ if (hasTooMany) {
+ Some(RejectRequest(TooManyRequests, message))
+ } else None
+ }
}
/**
@@ -293,32 +304,36 @@ protected[core] abstract class EntitlementProvider(config: WhiskConfig, loadBala
*/
trait ReferencedEntities {
- /**
- * Gathers referenced resources for types knows to refer to others.
- * This is usually done on a PUT request, hence the types are not one of the
- * canonical datastore types. Hence this method accepts Any reference but is
- * only defined for WhiskPackagePut, WhiskRulePut, and SequenceExec.
- *
- * It is plausible to lift these disambiguation below to a new trait which is
- * implemented by these types - however this will require exposing the Resource
- * type outside of the controller which is not yet desirable (although this could
- * cause further consolidation of the WhiskEntity and Resource types).
- *
- * @return Set of Resource instances if there are referenced entities.
- */
- def referencedEntities(reference: Any): Set[Resource] = {
- reference match {
- case WhiskPackagePut(Some(binding), _, _, _, _) =>
- Set(Resource(binding.namespace.toPath, Collection(Collection.PACKAGES), Some(binding.name.asString)))
- case r: WhiskRulePut =>
- val triggerResource = r.trigger.map { t => Resource(t.path, Collection(Collection.TRIGGERS), Some(t.name.asString)) }
- val actionResource = r.action map { a => Resource(a.path, Collection(Collection.ACTIONS), Some(a.name.asString)) }
- Set(triggerResource, actionResource).flatten
- case e: SequenceExec =>
- e.components.map {
- c => Resource(c.path, Collection(Collection.ACTIONS), Some(c.name.asString))
- }.toSet
- case _ => Set()
+ /**
+ * Gathers referenced resources for types knows to refer to others.
+ * This is usually done on a PUT request, hence the types are not one of the
+ * canonical datastore types. Hence this method accepts Any reference but is
+ * only defined for WhiskPackagePut, WhiskRulePut, and SequenceExec.
+ *
+ * It is plausible to lift these disambiguation below to a new trait which is
+ * implemented by these types - however this will require exposing the Resource
+ * type outside of the controller which is not yet desirable (although this could
+ * cause further consolidation of the WhiskEntity and Resource types).
+ *
+ * @return Set of Resource instances if there are referenced entities.
+ */
+ def referencedEntities(reference: Any): Set[Resource] = {
+ reference match {
+ case WhiskPackagePut(Some(binding), _, _, _, _) =>
+ Set(Resource(binding.namespace.toPath, Collection(Collection.PACKAGES), Some(binding.name.asString)))
+ case r: WhiskRulePut =>
+ val triggerResource = r.trigger.map { t =>
+ Resource(t.path, Collection(Collection.TRIGGERS), Some(t.name.asString))
+ }
+ val actionResource = r.action map { a =>
+ Resource(a.path, Collection(Collection.ACTIONS), Some(a.name.asString))
}
+ Set(triggerResource, actionResource).flatten
+ case e: SequenceExec =>
+ e.components.map { c =>
+ Resource(c.path, Collection(Collection.ACTIONS), Some(c.name.asString))
+ }.toSet
+ case _ => Set()
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/LocalEntitlement.scala b/core/controller/src/main/scala/whisk/core/entitlement/LocalEntitlement.scala
index 740d60a..d7ad703 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/LocalEntitlement.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/LocalEntitlement.scala
@@ -30,45 +30,47 @@ import whisk.core.entity.Subject
import whisk.core.loadBalancer.LoadBalancer
private object LocalEntitlementProvider {
- /** Poor mans entitlement matrix. Must persist to datastore eventually. */
- private val matrix = TrieMap[(Subject, String), Set[Privilege]]()
+
+ /** Poor mans entitlement matrix. Must persist to datastore eventually. */
+ private val matrix = TrieMap[(Subject, String), Set[Privilege]]()
}
-protected[core] class LocalEntitlementProvider(
- private val config: WhiskConfig,
- private val loadBalancer: LoadBalancer)(
- implicit actorSystem: ActorSystem,
- logging: Logging)
+protected[core] class LocalEntitlementProvider(private val config: WhiskConfig, private val loadBalancer: LoadBalancer)(
+ implicit actorSystem: ActorSystem,
+ logging: Logging)
extends EntitlementProvider(config, loadBalancer) {
- private implicit val executionContext = actorSystem.dispatcher
+ private implicit val executionContext = actorSystem.dispatcher
- private val matrix = LocalEntitlementProvider.matrix
+ private val matrix = LocalEntitlementProvider.matrix
- /** Grants subject right to resource by adding them to the entitlement matrix. */
- protected[core] override def grant(subject: Subject, right: Privilege, resource: Resource)(implicit transid: TransactionId) = Future {
- synchronized {
- val key = (subject, resource.id)
- matrix.put(key, matrix.get(key) map { _ + right } getOrElse Set(right))
- logging.info(this, s"granted user '$subject' privilege '$right' for '$resource'")
- true
- }
+ /** Grants subject right to resource by adding them to the entitlement matrix. */
+ protected[core] override def grant(subject: Subject, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = Future {
+ synchronized {
+ val key = (subject, resource.id)
+ matrix.put(key, matrix.get(key) map { _ + right } getOrElse Set(right))
+ logging.info(this, s"granted user '$subject' privilege '$right' for '$resource'")
+ true
}
+ }
- /** Revokes subject right to resource by removing them from the entitlement matrix. */
- protected[core] override def revoke(subject: Subject, right: Privilege, resource: Resource)(implicit transid: TransactionId) = Future {
- synchronized {
- val key = (subject, resource.id)
- val newrights = matrix.get(key) map { _ - right } map { matrix.put(key, _) }
- logging.info(this, s"revoked user '$subject' privilege '$right' for '$resource'")
- true
- }
+ /** Revokes subject right to resource by removing them from the entitlement matrix. */
+ protected[core] override def revoke(subject: Subject, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = Future {
+ synchronized {
+ val key = (subject, resource.id)
+ val newrights = matrix.get(key) map { _ - right } map { matrix.put(key, _) }
+ logging.info(this, s"revoked user '$subject' privilege '$right' for '$resource'")
+ true
}
+ }
- /** Checks if subject has explicit grant for a resource. */
- protected override def entitled(subject: Subject, right: Privilege, resource: Resource)(implicit transid: TransactionId) = Future.successful {
- lazy val one = matrix.get((subject, resource.id)) map { _ contains right } getOrElse false
- lazy val any = matrix.get((subject, resource.parent)) map { _ contains right } getOrElse false
- one || any
- }
+ /** Checks if subject has explicit grant for a resource. */
+ protected override def entitled(subject: Subject, right: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = Future.successful {
+ lazy val one = matrix.get((subject, resource.id)) map { _ contains right } getOrElse false
+ lazy val any = matrix.get((subject, resource.parent)) map { _ contains right } getOrElse false
+ one || any
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/PackageCollection.scala b/core/controller/src/main/scala/whisk/core/entitlement/PackageCollection.scala
index ce4f6ea..8d8a40f 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/PackageCollection.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/PackageCollection.scala
@@ -34,101 +34,105 @@ import whisk.http.Messages
class PackageCollection(entityStore: EntityStore)(implicit logging: Logging) extends Collection(Collection.PACKAGES) {
- protected override val allowedEntityRights = {
- Set(Privilege.READ, Privilege.PUT, Privilege.DELETE)
- }
+ protected override val allowedEntityRights = {
+ Set(Privilege.READ, Privilege.PUT, Privilege.DELETE)
+ }
- /**
- * Computes implicit rights on a package/binding.
- *
- * Must fetch the resource (a package or binding) to determine if it is in allowed namespaces.
- * There are two cases:
- *
- * 1. the resource is a package: then either it is in allowed namespaces or it is public.
- * 2. the resource is a binding: then it must be in allowed namespaces and (1) must hold for
- * the referenced package.
- *
- * A published package makes all its assets public regardless of their shared bit.
- * All assets that are not in an explicit package are private because the default package is private.
- */
- protected[core] override def implicitRights(user: Identity, namespaces: Set[String], right: Privilege, resource: Resource)(
- implicit ep: EntitlementProvider, ec: ExecutionContext, transid: TransactionId): Future[Boolean] = {
- resource.entity map {
- pkgname =>
- val isOwner = namespaces.contains(resource.namespace.root.asString)
- right match {
- case Privilege.READ =>
- // must determine if this is a public or owned package
- // or, for a binding, that it references a public or owned package
- val docid = FullyQualifiedEntityName(resource.namespace.root.toPath, EntityName(pkgname)).toDocId
- checkPackageReadPermission(namespaces, isOwner, docid)
- case _ => Future.successful(isOwner && allowedEntityRights.contains(right))
- }
- } getOrElse {
- // only a READ on the package collection is permitted;
- // NOTE: currently, the implementation allows any subject to
- // list packages in any namespace, and defers the filtering of
- // public packages to non-owning subjects to the API handlers
- // for packages
- Future.successful(right == Privilege.READ)
- }
+ /**
+ * Computes implicit rights on a package/binding.
+ *
+ * Must fetch the resource (a package or binding) to determine if it is in allowed namespaces.
+ * There are two cases:
+ *
+ * 1. the resource is a package: then either it is in allowed namespaces or it is public.
+ * 2. the resource is a binding: then it must be in allowed namespaces and (1) must hold for
+ * the referenced package.
+ *
+ * A published package makes all its assets public regardless of their shared bit.
+ * All assets that are not in an explicit package are private because the default package is private.
+ */
+ protected[core] override def implicitRights(user: Identity,
+ namespaces: Set[String],
+ right: Privilege,
+ resource: Resource)(implicit ep: EntitlementProvider,
+ ec: ExecutionContext,
+ transid: TransactionId): Future[Boolean] = {
+ resource.entity map { pkgname =>
+ val isOwner = namespaces.contains(resource.namespace.root.asString)
+ right match {
+ case Privilege.READ =>
+ // must determine if this is a public or owned package
+ // or, for a binding, that it references a public or owned package
+ val docid = FullyQualifiedEntityName(resource.namespace.root.toPath, EntityName(pkgname)).toDocId
+ checkPackageReadPermission(namespaces, isOwner, docid)
+ case _ => Future.successful(isOwner && allowedEntityRights.contains(right))
+ }
+ } getOrElse {
+ // only a READ on the package collection is permitted;
+ // NOTE: currently, the implementation allows any subject to
+ // list packages in any namespace, and defers the filtering of
+ // public packages to non-owning subjects to the API handlers
+ // for packages
+ Future.successful(right == Privilege.READ)
}
+ }
- /**
- * @param namespaces the set of namespaces the subject is entitled to
- * @param isOwner indicates if the resource is owned by the subject requesting authorization
- * @param docid the package (or binding) document id
- */
- private def checkPackageReadPermission(namespaces: Set[String], isOwner: Boolean, doc: DocId)(
- implicit ec: ExecutionContext, transid: TransactionId): Future[Boolean] = {
+ /**
+ * @param namespaces the set of namespaces the subject is entitled to
+ * @param isOwner indicates if the resource is owned by the subject requesting authorization
+ * @param docid the package (or binding) document id
+ */
+ private def checkPackageReadPermission(namespaces: Set[String], isOwner: Boolean, doc: DocId)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId): Future[Boolean] = {
- val right = Privilege.READ
+ val right = Privilege.READ
- WhiskPackage.get(entityStore, doc) flatMap {
- case wp if wp.binding.isEmpty =>
- val allowed = wp.publish || isOwner
- logging.info(this, s"entitlement check on package, '$right' allowed?: $allowed")
- Future.successful(allowed)
- case wp =>
- if (isOwner) {
- val binding = wp.binding.get
- val pkgOwner = namespaces.contains(binding.namespace.asString)
- val pkgDocid = binding.docid
- logging.info(this, s"checking subject has privilege '$right' for bound package '$pkgDocid'")
- checkPackageReadPermission(namespaces, pkgOwner, pkgDocid)
- } else {
- logging.info(this, s"entitlement check on package binding, '$right' allowed?: false")
- Future.successful(false)
- }
- } recoverWith {
- case t: NoDocumentException =>
- logging.info(this, s"the package does not exist (owner? $isOwner)")
- // if owner, reject with not found, otherwise fail the future to reject with
- // unauthorized (this prevents information leaks about packages in other namespaces)
- if (isOwner) {
- Future.failed(RejectRequest(NotFound))
- } else {
- Future.successful(false)
- }
- case t: DocumentTypeMismatchException =>
- logging.info(this, s"the requested binding is not a package (owner? $isOwner)")
- // if owner, reject with not found, otherwise fail the future to reject with
- // unauthorized (this prevents information leaks about packages in other namespaces)
- if (isOwner) {
- Future.failed(RejectRequest(Conflict, Messages.conformanceMessage))
- } else {
- Future.successful(false)
- }
- case t: RejectRequest =>
- logging.error(this, s"entitlement check on package failed: $t")
- Future.failed(t)
- case t =>
- logging.error(this, s"entitlement check on package failed: ${t.getMessage}")
- if (isOwner) {
- Future.failed(RejectRequest(InternalServerError, Messages.corruptedEntity))
- } else {
- Future.successful(false)
- }
+ WhiskPackage.get(entityStore, doc) flatMap {
+ case wp if wp.binding.isEmpty =>
+ val allowed = wp.publish || isOwner
+ logging.info(this, s"entitlement check on package, '$right' allowed?: $allowed")
+ Future.successful(allowed)
+ case wp =>
+ if (isOwner) {
+ val binding = wp.binding.get
+ val pkgOwner = namespaces.contains(binding.namespace.asString)
+ val pkgDocid = binding.docid
+ logging.info(this, s"checking subject has privilege '$right' for bound package '$pkgDocid'")
+ checkPackageReadPermission(namespaces, pkgOwner, pkgDocid)
+ } else {
+ logging.info(this, s"entitlement check on package binding, '$right' allowed?: false")
+ Future.successful(false)
+ }
+ } recoverWith {
+ case t: NoDocumentException =>
+ logging.info(this, s"the package does not exist (owner? $isOwner)")
+ // if owner, reject with not found, otherwise fail the future to reject with
+ // unauthorized (this prevents information leaks about packages in other namespaces)
+ if (isOwner) {
+ Future.failed(RejectRequest(NotFound))
+ } else {
+ Future.successful(false)
+ }
+ case t: DocumentTypeMismatchException =>
+ logging.info(this, s"the requested binding is not a package (owner? $isOwner)")
+ // if owner, reject with not found, otherwise fail the future to reject with
+ // unauthorized (this prevents information leaks about packages in other namespaces)
+ if (isOwner) {
+ Future.failed(RejectRequest(Conflict, Messages.conformanceMessage))
+ } else {
+ Future.successful(false)
+ }
+ case t: RejectRequest =>
+ logging.error(this, s"entitlement check on package failed: $t")
+ Future.failed(t)
+ case t =>
+ logging.error(this, s"entitlement check on package failed: ${t.getMessage}")
+ if (isOwner) {
+ Future.failed(RejectRequest(InternalServerError, Messages.corruptedEntity))
+ } else {
+ Future.successful(false)
}
}
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/RateThrottler.scala b/core/controller/src/main/scala/whisk/core/entitlement/RateThrottler.scala
index acd0230..770d78f 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/RateThrottler.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/RateThrottler.scala
@@ -29,61 +29,64 @@ import whisk.core.entity.UUID
*
* For now, we throttle only at a 1-minute granularity.
*/
-class RateThrottler(description: String, defaultMaxPerMinute: Int, overrideMaxPerMinute: Identity => Option[Int])(implicit logging: Logging) {
+class RateThrottler(description: String, defaultMaxPerMinute: Int, overrideMaxPerMinute: Identity => Option[Int])(
+ implicit logging: Logging) {
- logging.info(this, s"$description: defaultMaxPerMinute = $defaultMaxPerMinute")(TransactionId.controller)
+ logging.info(this, s"$description: defaultMaxPerMinute = $defaultMaxPerMinute")(TransactionId.controller)
- /**
- * Maintains map of subject namespace to operations rates.
- */
- private val rateMap = new TrieMap[UUID, RateInfo]
+ /**
+ * Maintains map of subject namespace to operations rates.
+ */
+ private val rateMap = new TrieMap[UUID, RateInfo]
- /**
- * Checks whether the operation should be allowed to proceed.
- * Every `check` operation charges the subject namespace for one operation.
- *
- * @param user the identity to check
- * @return true iff subject namespace is below allowed limit
- */
- def check(user: Identity)(implicit transid: TransactionId): Boolean = {
- val uuid = user.uuid // this is namespace identifier
- val rate = rateMap.getOrElseUpdate(uuid, new RateInfo)
- val limit = overrideMaxPerMinute(user).getOrElse(defaultMaxPerMinute)
- val belowLimit = rate.check(limit)
- logging.debug(this, s"namespace = ${uuid.asString} rate = ${rate.count()}, limit = $limit, below limit = $belowLimit")
- belowLimit
- }
+ /**
+ * Checks whether the operation should be allowed to proceed.
+ * Every `check` operation charges the subject namespace for one operation.
+ *
+ * @param user the identity to check
+ * @return true iff subject namespace is below allowed limit
+ */
+ def check(user: Identity)(implicit transid: TransactionId): Boolean = {
+ val uuid = user.uuid // this is namespace identifier
+ val rate = rateMap.getOrElseUpdate(uuid, new RateInfo)
+ val limit = overrideMaxPerMinute(user).getOrElse(defaultMaxPerMinute)
+ val belowLimit = rate.check(limit)
+ logging.debug(
+ this,
+ s"namespace = ${uuid.asString} rate = ${rate.count()}, limit = $limit, below limit = $belowLimit")
+ belowLimit
+ }
}
/**
* Tracks the activation rate of one subject at minute-granularity.
*/
private class RateInfo {
- var lastMin = getCurrentMinute
- var lastMinCount = 0
+ var lastMin = getCurrentMinute
+ var lastMinCount = 0
- def count() = lastMinCount
+ def count() = lastMinCount
- /**
- * Increments operation count in the current time window by
- * one and checks if below allowed max rate.
- *
- * @param maxPerMinute the current maximum allowed requests
- * per minute (might change over time)
- */
- def check(maxPerMinute: Int): Boolean = {
- roll()
- lastMinCount = lastMinCount + 1
- lastMinCount <= maxPerMinute
- }
+ /**
+ * Increments operation count in the current time window by
+ * one and checks if below allowed max rate.
+ *
+ * @param maxPerMinute the current maximum allowed requests
+ * per minute (might change over time)
+ */
+ def check(maxPerMinute: Int): Boolean = {
+ roll()
+ lastMinCount = lastMinCount + 1
+ lastMinCount <= maxPerMinute
+ }
- def roll() = {
- val curMin = getCurrentMinute
- if (curMin != lastMin) {
- lastMin = curMin
- lastMinCount = 0
- }
+ def roll() = {
+ val curMin = getCurrentMinute
+ if (curMin != lastMin) {
+ lastMin = curMin
+ lastMinCount = 0
}
+ }
- private def getCurrentMinute = System.currentTimeMillis / (60 * 1000)
+ private def getCurrentMinute = System.currentTimeMillis / (60 * 1000)
}
diff --git a/core/controller/src/main/scala/whisk/core/loadBalancer/InvokerSupervision.scala b/core/controller/src/main/scala/whisk/core/loadBalancer/InvokerSupervision.scala
index 2ae224b..0a9f13a7 100644
--- a/core/controller/src/main/scala/whisk/core/loadBalancer/InvokerSupervision.scala
+++ b/core/controller/src/main/scala/whisk/core/loadBalancer/InvokerSupervision.scala
@@ -75,109 +75,115 @@ final case class InvokerInfo(buffer: RingBuffer[Boolean])
* Note: An Invoker that never sends an initial Ping will not be considered
* by the InvokerPool and thus might not be caught by monitoring.
*/
-class InvokerPool(
- childFactory: (ActorRefFactory, InstanceId) => ActorRef,
- sendActivationToInvoker: (ActivationMessage, InstanceId) => Future[RecordMetadata],
- pingConsumer: MessageConsumer) extends Actor {
-
- implicit val transid = TransactionId.invokerHealth
- implicit val logging = new AkkaLogging(context.system.log)
- implicit val timeout = Timeout(5.seconds)
- implicit val ec = context.dispatcher
-
- // State of the actor. It's important not to close over these
- // references directly, so they don't escape the Actor.
- val instanceToRef = mutable.Map[InstanceId, ActorRef]()
- val refToInstance = mutable.Map[ActorRef, InstanceId]()
- var status = IndexedSeq[(InstanceId, InvokerState)]()
-
- def receive = {
- case p: PingMessage =>
- val invoker = instanceToRef.getOrElseUpdate(p.instance, {
- logging.info(this, s"registered a new invoker: invoker${p.instance.toInt}")(TransactionId.invokerHealth)
-
- status = padToIndexed(status, p.instance.toInt + 1, i => (InstanceId(i), Offline))
-
- val ref = childFactory(context, p.instance)
- ref ! SubscribeTransitionCallBack(self) // register for state change events
-
- refToInstance.update(ref, p.instance)
- ref
- })
- invoker.forward(p)
-
- case GetStatus => sender() ! status
-
- case msg: InvocationFinishedMessage => {
- // Forward message to invoker, if InvokerActor exists
- instanceToRef.get(msg.invokerInstance).map(_.forward(msg))
- }
-
- case CurrentState(invoker, currentState: InvokerState) =>
- refToInstance.get(invoker).foreach { instance =>
- status = status.updated(instance.toInt, (instance, currentState))
- }
- logStatus()
-
- case Transition(invoker, oldState: InvokerState, newState: InvokerState) =>
- refToInstance.get(invoker).foreach {
- instance => status = status.updated(instance.toInt, (instance, newState))
- }
- logStatus()
-
- // this is only used for the internal test action which enabled an invoker to become healthy again
- case msg: ActivationRequest => sendActivationToInvoker(msg.msg, msg.invoker).pipeTo(sender)
+class InvokerPool(childFactory: (ActorRefFactory, InstanceId) => ActorRef,
+ sendActivationToInvoker: (ActivationMessage, InstanceId) => Future[RecordMetadata],
+ pingConsumer: MessageConsumer)
+ extends Actor {
+
+ implicit val transid = TransactionId.invokerHealth
+ implicit val logging = new AkkaLogging(context.system.log)
+ implicit val timeout = Timeout(5.seconds)
+ implicit val ec = context.dispatcher
+
+ // State of the actor. It's important not to close over these
+ // references directly, so they don't escape the Actor.
+ val instanceToRef = mutable.Map[InstanceId, ActorRef]()
+ val refToInstance = mutable.Map[ActorRef, InstanceId]()
+ var status = IndexedSeq[(InstanceId, InvokerState)]()
+
+ def receive = {
+ case p: PingMessage =>
+ val invoker = instanceToRef.getOrElseUpdate(p.instance, {
+ logging.info(this, s"registered a new invoker: invoker${p.instance.toInt}")(TransactionId.invokerHealth)
+
+ status = padToIndexed(status, p.instance.toInt + 1, i => (InstanceId(i), Offline))
+
+ val ref = childFactory(context, p.instance)
+ ref ! SubscribeTransitionCallBack(self) // register for state change events
+
+ refToInstance.update(ref, p.instance)
+ ref
+ })
+ invoker.forward(p)
+
+ case GetStatus => sender() ! status
+
+ case msg: InvocationFinishedMessage => {
+ // Forward message to invoker, if InvokerActor exists
+ instanceToRef.get(msg.invokerInstance).map(_.forward(msg))
}
- def logStatus() = {
- val pretty = status.map { case (instance, state) => s"${instance.toInt} -> $state" }
- logging.info(this, s"invoker status changed to ${pretty.mkString(", ")}")
+ case CurrentState(invoker, currentState: InvokerState) =>
+ refToInstance.get(invoker).foreach { instance =>
+ status = status.updated(instance.toInt, (instance, currentState))
+ }
+ logStatus()
+
+ case Transition(invoker, oldState: InvokerState, newState: InvokerState) =>
+ refToInstance.get(invoker).foreach { instance =>
+ status = status.updated(instance.toInt, (instance, newState))
+ }
+ logStatus()
+
+ // this is only used for the internal test action which enabled an invoker to become healthy again
+ case msg: ActivationRequest => sendActivationToInvoker(msg.msg, msg.invoker).pipeTo(sender)
+ }
+
+ def logStatus() = {
+ val pretty = status.map { case (instance, state) => s"${instance.toInt} -> $state" }
+ logging.info(this, s"invoker status changed to ${pretty.mkString(", ")}")
+ }
+
+ /** Receive Ping messages from invokers. */
+ val pingPollDuration = 1.second
+ val invokerPingFeed = context.system.actorOf(Props {
+ new MessageFeed(
+ "ping",
+ logging,
+ pingConsumer,
+ pingConsumer.maxPeek,
+ pingPollDuration,
+ processInvokerPing,
+ logHandoff = false)
+ })
+
+ def processInvokerPing(bytes: Array[Byte]): Future[Unit] = Future {
+ val raw = new String(bytes, StandardCharsets.UTF_8)
+ PingMessage.parse(raw) match {
+ case Success(p: PingMessage) =>
+ self ! p
+ invokerPingFeed ! MessageFeed.Processed
+
+ case Failure(t) =>
+ invokerPingFeed ! MessageFeed.Processed
+ logging.error(this, s"failed processing message: $raw with $t")
}
+ }
- /** Receive Ping messages from invokers. */
- val pingPollDuration = 1.second
- val invokerPingFeed = context.system.actorOf(Props {
- new MessageFeed("ping", logging, pingConsumer, pingConsumer.maxPeek, pingPollDuration, processInvokerPing, logHandoff = false)
- })
-
- def processInvokerPing(bytes: Array[Byte]): Future[Unit] = Future {
- val raw = new String(bytes, StandardCharsets.UTF_8)
- PingMessage.parse(raw) match {
- case Success(p: PingMessage) =>
- self ! p
- invokerPingFeed ! MessageFeed.Processed
-
- case Failure(t) =>
- invokerPingFeed ! MessageFeed.Processed
- logging.error(this, s"failed processing message: $raw with $t")
- }
- }
-
- /** Pads a list to a given length using the given function to compute entries */
- def padToIndexed[A](list: IndexedSeq[A], n: Int, f: (Int) => A) = list ++ (list.size until n).map(f)
+ /** Pads a list to a given length using the given function to compute entries */
+ def padToIndexed[A](list: IndexedSeq[A], n: Int, f: (Int) => A) = list ++ (list.size until n).map(f)
}
object InvokerPool {
- def props(
- f: (ActorRefFactory, InstanceId) => ActorRef,
- p: (ActivationMessage, InstanceId) => Future[RecordMetadata],
- pc: MessageConsumer) = {
- Props(new InvokerPool(f, p, pc))
- }
-
- /** A stub identity for invoking the test action. This does not need to be a valid identity. */
- val healthActionIdentity = {
- val whiskSystem = "whisk.system"
- Identity(Subject(whiskSystem), EntityName(whiskSystem), AuthKey(UUID(), Secret()), Set[Privilege]())
- }
-
- /** An action to use for monitoring invoker health. */
- def healthAction(i: InstanceId) = ExecManifest.runtimesManifest.resolveDefaultRuntime("nodejs:6").map { manifest =>
- new WhiskAction(
- namespace = healthActionIdentity.namespace.toPath,
- name = EntityName(s"invokerHealthTestAction${i.toInt}"),
- exec = new CodeExecAsString(manifest, """function main(params) { return params; }""", None))
- }
+ def props(f: (ActorRefFactory, InstanceId) => ActorRef,
+ p: (ActivationMessage, InstanceId) => Future[RecordMetadata],
+ pc: MessageConsumer) = {
+ Props(new InvokerPool(f, p, pc))
+ }
+
+ /** A stub identity for invoking the test action. This does not need to be a valid identity. */
+ val healthActionIdentity = {
+ val whiskSystem = "whisk.system"
+ Identity(Subject(whiskSystem), EntityName(whiskSystem), AuthKey(UUID(), Secret()), Set[Privilege]())
+ }
+
+ /** An action to use for monitoring invoker health. */
+ def healthAction(i: InstanceId) = ExecManifest.runtimesManifest.resolveDefaultRuntime("nodejs:6").map { manifest =>
+ new WhiskAction(
+ namespace = healthActionIdentity.namespace.toPath,
+ name = EntityName(s"invokerHealthTestAction${i.toInt}"),
+ exec = new CodeExecAsString(manifest, """function main(params) { return params; }""", None))
+ }
}
/**
@@ -187,149 +193,160 @@ object InvokerPool {
* states "Healthy" and "Offline".
*/
class InvokerActor(invokerInstance: InstanceId, controllerInstance: InstanceId) extends FSM[InvokerState, InvokerInfo] {
- implicit val transid = TransactionId.invokerHealth
- implicit val logging = new AkkaLogging(context.system.log)
- val name = s"invoker${invokerInstance.toInt}"
-
- val healthyTimeout = 10.seconds
-
- // This is done at this point to not intermingle with the state-machine
- // especially their timeouts.
- def customReceive: Receive = {
- case _: RecordMetadata => // The response of putting testactions to the MessageProducer. We don't have to do anything with them.
+ implicit val transid = TransactionId.invokerHealth
+ implicit val logging = new AkkaLogging(context.system.log)
+ val name = s"invoker${invokerInstance.toInt}"
+
+ val healthyTimeout = 10.seconds
+
+ // This is done at this point to not intermingle with the state-machine
+ // especially their timeouts.
+ def customReceive: Receive = {
+ case _: RecordMetadata => // The response of putting testactions to the MessageProducer. We don't have to do anything with them.
+ }
+ override def receive = customReceive.orElse(super.receive)
+
+ /**
+ * Always start UnHealthy. Then the invoker receives some test activations and becomes Healthy.
+ */
+ startWith(UnHealthy, InvokerInfo(new RingBuffer[Boolean](InvokerActor.bufferSize)))
+
+ /**
+ * An Offline invoker represents an existing but broken
+ * invoker. This means, that it does not send pings anymore.
+ */
+ when(Offline) {
+ case Event(_: PingMessage, _) => goto(UnHealthy)
+ }
+
+ /**
+ * An UnHealthy invoker represents an invoker that was not able to handle actions successfully.
+ */
+ when(UnHealthy, stateTimeout = healthyTimeout) {
+ case Event(_: PingMessage, _) => stay
+ case Event(StateTimeout, _) => goto(Offline)
+ case Event(Tick, info) => {
+ invokeTestAction()
+ stay
}
- override def receive = customReceive.orElse(super.receive)
-
- /**
- * Always start UnHealthy. Then the invoker receives some test activations and becomes Healthy.
- */
- startWith(UnHealthy, InvokerInfo(new RingBuffer[Boolean](InvokerActor.bufferSize)))
-
- /**
- * An Offline invoker represents an existing but broken
- * invoker. This means, that it does not send pings anymore.
- */
- when(Offline) {
- case Event(_: PingMessage, _) => goto(UnHealthy)
+ }
+
+ /**
+ * A Healthy invoker is characterized by continuously getting
+ * pings. It will go offline if that state is not confirmed
+ * for 20 seconds.
+ */
+ when(Healthy, stateTimeout = healthyTimeout) {
+ case Event(_: PingMessage, _) => stay
+ case Event(StateTimeout, _) => goto(Offline)
+ }
+
+ /**
+ * Handle the completion of an Activation in every state.
+ */
+ whenUnhandled {
+ case Event(cm: InvocationFinishedMessage, info) => handleCompletionMessage(cm.successful, info.buffer)
+ }
+
+ /** Logging on Transition change */
+ onTransition {
+ case _ -> Offline =>
+ transid.mark(
+ this,
+ LoggingMarkers.LOADBALANCER_INVOKER_OFFLINE,
+ s"$name is offline",
+ akka.event.Logging.WarningLevel)
+ case _ -> UnHealthy =>
+ transid.mark(
+ this,
+ LoggingMarkers.LOADBALANCER_INVOKER_UNHEALTHY,
+ s"$name is unhealthy",
+ akka.event.Logging.WarningLevel)
+ case _ -> Healthy => logging.info(this, s"$name is healthy")
+ }
+
+ /** Scheduler to send test activations when the invoker is unhealthy. */
+ onTransition {
+ case _ -> UnHealthy => {
+ invokeTestAction()
+ setTimer(InvokerActor.timerName, Tick, 1.minute, true)
}
-
- /**
- * An UnHealthy invoker represents an invoker that was not able to handle actions successfully.
- */
- when(UnHealthy, stateTimeout = healthyTimeout) {
- case Event(_: PingMessage, _) => stay
- case Event(StateTimeout, _) => goto(Offline)
- case Event(Tick, info) => {
- invokeTestAction()
- stay
- }
+ case UnHealthy -> _ => cancelTimer(InvokerActor.timerName)
+ }
+
+ initialize()
+
+ /**
+ * Handling for active acks. This method saves the result (successful or unsuccessful)
+ * into an RingBuffer and checks, if the InvokerActor has to be changed to UnHealthy.
+ *
+ * @param wasActivationSuccessful: result of Activation
+ * @param buffer to be used
+ */
+ private def handleCompletionMessage(wasActivationSuccessful: Boolean, buffer: RingBuffer[Boolean]) = {
+ buffer.add(wasActivationSuccessful)
+
+ // If the current state is UnHealthy, then the active ack is the result of a test action.
+ // If this is successful it seems like the Invoker is Healthy again. So we execute immediately
+ // a new test action to remove the errors out of the RingBuffer as fast as possible.
+ if (wasActivationSuccessful && stateName == UnHealthy) {
+ invokeTestAction()
}
- /**
- * A Healthy invoker is characterized by continuously getting
- * pings. It will go offline if that state is not confirmed
- * for 20 seconds.
- */
- when(Healthy, stateTimeout = healthyTimeout) {
- case Event(_: PingMessage, _) => stay
- case Event(StateTimeout, _) => goto(Offline)
+ // Stay in online if the activations was successful.
+ // Stay in offline, if an activeAck reaches the controller.
+ if ((stateName == Healthy && wasActivationSuccessful) || stateName == Offline) {
+ stay
+ } else {
+ // Goto UnHealthy if there are more errors than accepted in buffer, else goto Healthy
+ if (buffer.toList.count(_ == true) >= InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance) {
+ gotoIfNotThere(Healthy)
+ } else {
+ gotoIfNotThere(UnHealthy)
+ }
}
-
- /**
- * Handle the completion of an Activation in every state.
- */
- whenUnhandled {
- case Event(cm: InvocationFinishedMessage, info) => handleCompletionMessage(cm.successful, info.buffer)
- }
-
- /** Logging on Transition change */
- onTransition {
- case _ -> Offline => transid.mark(this, LoggingMarkers.LOADBALANCER_INVOKER_OFFLINE, s"$name is offline", akka.event.Logging.WarningLevel)
- case _ -> UnHealthy => transid.mark(this, LoggingMarkers.LOADBALANCER_INVOKER_UNHEALTHY, s"$name is unhealthy", akka.event.Logging.WarningLevel)
- case _ -> Healthy => logging.info(this, s"$name is healthy")
- }
-
- /** Scheduler to send test activations when the invoker is unhealthy. */
- onTransition {
- case _ -> UnHealthy => {
- invokeTestAction()
- setTimer(InvokerActor.timerName, Tick, 1.minute, true)
- }
- case UnHealthy -> _ => cancelTimer(InvokerActor.timerName)
- }
-
- initialize()
-
- /**
- * Handling for active acks. This method saves the result (successful or unsuccessful)
- * into an RingBuffer and checks, if the InvokerActor has to be changed to UnHealthy.
- *
- * @param wasActivationSuccessful: result of Activation
- * @param buffer to be used
- */
- private def handleCompletionMessage(wasActivationSuccessful: Boolean, buffer: RingBuffer[Boolean]) = {
- buffer.add(wasActivationSuccessful)
-
- // If the current state is UnHealthy, then the active ack is the result of a test action.
- // If this is successful it seems like the Invoker is Healthy again. So we execute immediately
- // a new test action to remove the errors out of the RingBuffer as fast as possible.
- if (wasActivationSuccessful && stateName == UnHealthy) {
- invokeTestAction()
- }
-
- // Stay in online if the activations was successful.
- // Stay in offline, if an activeAck reaches the controller.
- if ((stateName == Healthy && wasActivationSuccessful) || stateName == Offline) {
- stay
- } else {
- // Goto UnHealthy if there are more errors than accepted in buffer, else goto Healthy
- if (buffer.toList.count(_ == true) >= InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance) {
- gotoIfNotThere(Healthy)
- } else {
- gotoIfNotThere(UnHealthy)
- }
- }
- }
-
- /**
- * Creates an activation request with the given action and sends it to the InvokerPool.
- * The InvokerPool redirects it to the invoker which is represented by this InvokerActor.
- */
- private def invokeTestAction() = {
- InvokerPool.healthAction(controllerInstance).map { action =>
- val activationMessage = ActivationMessage(
- // Use the sid of the InvokerSupervisor as tid
- transid = transid,
- action = action.fullyQualifiedName(true),
- // Use empty DocRevision to force the invoker to pull the action from db all the time
- revision = DocRevision.empty,
- user = InvokerPool.healthActionIdentity,
- // Create a new Activation ID for this activation
- activationId = new ActivationIdGenerator {}.make(),
- activationNamespace = action.namespace,
- rootControllerIndex = controllerInstance,
- blocking = false,
- content = None)
-
- context.parent ! ActivationRequest(activationMessage, invokerInstance)
- }
- }
-
- /**
- * Only change the state if the currentState is not the newState.
- *
- * @param newState of the InvokerActor
- */
- private def gotoIfNotThere(newState: InvokerState) = {
- if (stateName == newState) stay() else goto(newState)
+ }
+
+ /**
+ * Creates an activation request with the given action and sends it to the InvokerPool.
+ * The InvokerPool redirects it to the invoker which is represented by this InvokerActor.
+ */
+ private def invokeTestAction() = {
+ InvokerPool.healthAction(controllerInstance).map { action =>
+ val activationMessage = ActivationMessage(
+ // Use the sid of the InvokerSupervisor as tid
+ transid = transid,
+ action = action.fullyQualifiedName(true),
+ // Use empty DocRevision to force the invoker to pull the action from db all the time
+ revision = DocRevision.empty,
+ user = InvokerPool.healthActionIdentity,
+ // Create a new Activation ID for this activation
+ activationId = new ActivationIdGenerator {}.make(),
+ activationNamespace = action.namespace,
+ rootControllerIndex = controllerInstance,
+ blocking = false,
+ content = None)
+
+ context.parent ! ActivationRequest(activationMessage, invokerInstance)
}
+ }
+
+ /**
+ * Only change the state if the currentState is not the newState.
+ *
+ * @param newState of the InvokerActor
+ */
+ private def gotoIfNotThere(newState: InvokerState) = {
+ if (stateName == newState) stay() else goto(newState)
+ }
}
object InvokerActor {
- def props(invokerInstance: InstanceId, controllerInstance: InstanceId) = Props(new InvokerActor(invokerInstance, controllerInstance))
+ def props(invokerInstance: InstanceId, controllerInstance: InstanceId) =
+ Props(new InvokerActor(invokerInstance, controllerInstance))
- val bufferSize = 10
- val bufferErrorTolerance = 3
+ val bufferSize = 10
+ val bufferErrorTolerance = 3
- val timerName = "testActionTimer"
+ val timerName = "testActionTimer"
}
diff --git a/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerData.scala b/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerData.scala
index 9014cc6..c693a8f 100644
--- a/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerData.scala
+++ b/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerData.scala
@@ -22,11 +22,14 @@ import java.util.concurrent.atomic.AtomicInteger
import scala.collection.concurrent.TrieMap
import scala.concurrent.Promise
-import whisk.core.entity.{ ActivationId, UUID, WhiskActivation }
+import whisk.core.entity.{ActivationId, UUID, WhiskActivation}
import whisk.core.entity.InstanceId
/** Encapsulates data relevant for a single activation */
-case class ActivationEntry(id: ActivationId, namespaceId: UUID, invokerName: InstanceId, promise: Promise[Either[ActivationId, WhiskActivation]])
+case class ActivationEntry(id: ActivationId,
+ namespaceId: UUID,
+ invokerName: InstanceId,
+ promise: Promise[Either[ActivationId, WhiskActivation]])
/**
* Encapsulates data used for loadbalancer and active-ack bookkeeping.
@@ -36,86 +39,86 @@ case class ActivationEntry(id: ActivationId, namespaceId: UUID, invokerName: Ins
*/
class LoadBalancerData() {
- private val activationByInvoker = TrieMap[InstanceId, AtomicInteger]()
- private val activationByNamespaceId = TrieMap[UUID, AtomicInteger]()
- private val activationsById = TrieMap[ActivationId, ActivationEntry]()
- private val totalActivations = new AtomicInteger(0)
+ private val activationByInvoker = TrieMap[InstanceId, AtomicInteger]()
+ private val activationByNamespaceId = TrieMap[UUID, AtomicInteger]()
+ private val activationsById = TrieMap[ActivationId, ActivationEntry]()
+ private val totalActivations = new AtomicInteger(0)
- /** Get the number of activations across all namespaces. */
- def totalActivationCount = totalActivations.get
+ /** Get the number of activations across all namespaces. */
+ def totalActivationCount = totalActivations.get
- /**
- * Get the number of activations for a specific namespace.
- *
- * @param namespace The namespace to get the activation count for
- * @return a map (namespace -> number of activations in the system)
- */
- def activationCountOn(namespace: UUID) = {
- activationByNamespaceId.get(namespace).map(_.get).getOrElse(0)
- }
+ /**
+ * Get the number of activations for a specific namespace.
+ *
+ * @param namespace The namespace to get the activation count for
+ * @return a map (namespace -> number of activations in the system)
+ */
+ def activationCountOn(namespace: UUID) = {
+ activationByNamespaceId.get(namespace).map(_.get).getOrElse(0)
+ }
- /**
- * Get the number of activations for a specific invoker.
- *
- * @param invoker The invoker to get the activation count for
- * @return a map (invoker -> number of activations queued for the invoker)
- */
- def activationCountOn(invoker: InstanceId): Int = {
- activationByInvoker.get(invoker).map(_.get).getOrElse(0)
- }
+ /**
+ * Get the number of activations for a specific invoker.
+ *
+ * @param invoker The invoker to get the activation count for
+ * @return a map (invoker -> number of activations queued for the invoker)
+ */
+ def activationCountOn(invoker: InstanceId): Int = {
+ activationByInvoker.get(invoker).map(_.get).getOrElse(0)
+ }
- /**
- * Get an activation entry for a given activation id.
- *
- * @param activationId activation id to get data for
- * @return the respective activation or None if it doesn't exist
- */
- def activationById(activationId: ActivationId): Option[ActivationEntry] = {
- activationsById.get(activationId)
- }
+ /**
+ * Get an activation entry for a given activation id.
+ *
+ * @param activationId activation id to get data for
+ * @return the respective activation or None if it doesn't exist
+ */
+ def activationById(activationId: ActivationId): Option[ActivationEntry] = {
+ activationsById.get(activationId)
+ }
- /**
- * Adds an activation entry.
- *
- * @param id identifier to deduplicate the entry
- * @param update block calculating the entry to add.
- * Note: This is evaluated iff the entry
- * didn't exist before.
- * @return the entry calculated by the block or iff it did
- * exist before the entry from the state
- */
- def putActivation(id: ActivationId, update: => ActivationEntry): ActivationEntry = {
- activationsById.getOrElseUpdate(id, {
- val entry = update
- totalActivations.incrementAndGet()
- activationByNamespaceId.getOrElseUpdate(entry.namespaceId, new AtomicInteger(0)).incrementAndGet()
- activationByInvoker.getOrElseUpdate(entry.invokerName, new AtomicInteger(0)).incrementAndGet()
- entry
- })
- }
+ /**
+ * Adds an activation entry.
+ *
+ * @param id identifier to deduplicate the entry
+ * @param update block calculating the entry to add.
+ * Note: This is evaluated iff the entry
+ * didn't exist before.
+ * @return the entry calculated by the block or iff it did
+ * exist before the entry from the state
+ */
+ def putActivation(id: ActivationId, update: => ActivationEntry): ActivationEntry = {
+ activationsById.getOrElseUpdate(id, {
+ val entry = update
+ totalActivations.incrementAndGet()
+ activationByNamespaceId.getOrElseUpdate(entry.namespaceId, new AtomicInteger(0)).incrementAndGet()
+ activationByInvoker.getOrElseUpdate(entry.invokerName, new AtomicInteger(0)).incrementAndGet()
+ entry
+ })
+ }
- /**
- * Removes the given entry.
- *
- * @param entry the entry to remove
- * @return The deleted entry or None if nothing got deleted
- */
- def removeActivation(entry: ActivationEntry): Option[ActivationEntry] = {
- activationsById.remove(entry.id).map { x =>
- totalActivations.decrementAndGet()
- activationByNamespaceId.getOrElseUpdate(entry.namespaceId, new AtomicInteger(0)).decrementAndGet()
- activationByInvoker.getOrElseUpdate(entry.invokerName, new AtomicInteger(0)).decrementAndGet()
- x
- }
+ /**
+ * Removes the given entry.
+ *
+ * @param entry the entry to remove
+ * @return The deleted entry or None if nothing got deleted
+ */
+ def removeActivation(entry: ActivationEntry): Option[ActivationEntry] = {
+ activationsById.remove(entry.id).map { x =>
+ totalActivations.decrementAndGet()
+ activationByNamespaceId.getOrElseUpdate(entry.namespaceId, new AtomicInteger(0)).decrementAndGet()
+ activationByInvoker.getOrElseUpdate(entry.invokerName, new AtomicInteger(0)).decrementAndGet()
+ x
}
+ }
- /**
- * Removes the activation identified by the given activation id.
- *
- * @param aid activation id to remove
- * @return The deleted entry or None if nothing got deleted
- */
- def removeActivation(aid: ActivationId): Option[ActivationEntry] = {
- activationsById.get(aid).flatMap(removeActivation)
- }
+ /**
+ * Removes the activation identified by the given activation id.
+ *
+ * @param aid activation id to remove
+ * @return The deleted entry or None if nothing got deleted
+ */
+ def removeActivation(aid: ActivationId): Option[ActivationEntry] = {
+ activationsById.get(aid).flatMap(removeActivation)
+ }
}
diff --git a/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala b/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
index 7684fa6..4efbad6 100644
--- a/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
+++ b/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
@@ -41,12 +41,12 @@ import whisk.common.LoggingMarkers
import whisk.common.TransactionId
import whisk.core.WhiskConfig
import whisk.core.WhiskConfig._
-import whisk.core.connector.{ ActivationMessage, CompletionMessage }
+import whisk.core.connector.{ActivationMessage, CompletionMessage}
import whisk.core.connector.MessageFeed
import whisk.core.connector.MessageProducer
import whisk.core.connector.MessagingProvider
import whisk.core.database.NoDocumentException
-import whisk.core.entity.{ ActivationId, WhiskActivation }
+import whisk.core.entity.{ActivationId, WhiskActivation}
import whisk.core.entity.EntityName
import whisk.core.entity.ExecutableWhiskAction
import whisk.core.entity.Identity
@@ -58,287 +58,318 @@ import whisk.spi.SpiLoader
trait LoadBalancer {
- val activeAckTimeoutGrace = 1.minute
-
- /** Gets the number of in-flight activations for a specific user. */
- def activeActivationsFor(namspace: UUID): Int
-
- /** Gets the number of in-flight activations in the system. */
- def totalActiveActivations: Int
-
- /**
- * Publishes activation message on internal bus for an invoker to pick up.
- *
- * @param action the action to invoke
- * @param msg the activation message to publish on an invoker topic
- * @param transid the transaction id for the request
- * @return result a nested Future the outer indicating completion of publishing and
- * the inner the completion of the action (i.e., the result)
- * if it is ready before timeout (Right) otherwise the activation id (Left).
- * The future is guaranteed to complete within the declared action time limit
- * plus a grace period (see activeAckTimeoutGrace).
- */
- def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]]
+ val activeAckTimeoutGrace = 1.minute
+
+ /** Gets the number of in-flight activations for a specific user. */
+ def activeActivationsFor(namspace: UUID): Int
+
+ /** Gets the number of in-flight activations in the system. */
+ def totalActiveActivations: Int
+
+ /**
+ * Publishes activation message on internal bus for an invoker to pick up.
+ *
+ * @param action the action to invoke
+ * @param msg the activation message to publish on an invoker topic
+ * @param transid the transaction id for the request
+ * @return result a nested Future the outer indicating completion of publishing and
+ * the inner the completion of the action (i.e., the result)
+ * if it is ready before timeout (Right) otherwise the activation id (Left).
+ * The future is guaranteed to complete within the declared action time limit
+ * plus a grace period (see activeAckTimeoutGrace).
+ */
+ def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(
+ implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]]
}
-class LoadBalancerService(
- config: WhiskConfig,
- instance: InstanceId,
- entityStore: EntityStore)(
- implicit val actorSystem: ActorSystem,
- logging: Logging)
+class LoadBalancerService(config: WhiskConfig, instance: InstanceId, entityStore: EntityStore)(
+ implicit val actorSystem: ActorSystem,
+ logging: Logging)
extends LoadBalancer {
- /** The execution context for futures */
- implicit val executionContext: ExecutionContext = actorSystem.dispatcher
-
- /** How many invokers are dedicated to blackbox images. We range bound to something sensical regardless of configuration. */
- private val blackboxFraction: Double = Math.max(0.0, Math.min(1.0, config.controllerBlackboxFraction))
- logging.info(this, s"blackboxFraction = $blackboxFraction")(TransactionId.loadbalancer)
-
- private val loadBalancerData = new LoadBalancerData()
+ /** The execution context for futures */
+ implicit val executionContext: ExecutionContext = actorSystem.dispatcher
- override def activeActivationsFor(namespace: UUID) = loadBalancerData.activationCountOn(namespace)
+ /** How many invokers are dedicated to blackbox images. We range bound to something sensical regardless of configuration. */
+ private val blackboxFraction: Double = Math.max(0.0, Math.min(1.0, config.controllerBlackboxFraction))
+ logging.info(this, s"blackboxFraction = $blackboxFraction")(TransactionId.loadbalancer)
- override def totalActiveActivations = loadBalancerData.totalActivationCount
+ private val loadBalancerData = new LoadBalancerData()
- override def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(
- implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] = {
- chooseInvoker(msg.user, action).flatMap { invokerName =>
- val entry = setupActivation(action, msg.activationId, msg.user.uuid, invokerName, transid)
- sendActivationToInvoker(messageProducer, msg, invokerName).map { _ =>
- entry.promise.future
- }
- }
- }
+ override def activeActivationsFor(namespace: UUID) = loadBalancerData.activationCountOn(namespace)
- /** An indexed sequence of all invokers in the current system */
- def allInvokers: Future[IndexedSeq[(InstanceId, InvokerState)]] = invokerPool
- .ask(GetStatus)(Timeout(5.seconds))
- .mapTo[IndexedSeq[(InstanceId, InvokerState)]]
-
- /**
- * Tries to fill in the result slot (i.e., complete the promise) when a completion message arrives.
- * The promise is removed form the map when the result arrives or upon timeout.
- *
- * @param msg is the kafka message payload as Json
- */
- private def processCompletion(response: Either[ActivationId, WhiskActivation], tid: TransactionId, forced: Boolean): Unit = {
- val aid = response.fold(l => l, r => r.activationId)
- loadBalancerData.removeActivation(aid) match {
- case Some(entry) =>
- logging.info(this, s"${if (!forced) "received" else "forced"} active ack for '$aid'")(tid)
- if (!forced) {
- entry.promise.trySuccess(response)
- } else {
- entry.promise.tryFailure(new Throwable("no active ack received"))
- }
- case None =>
- // the entry was already removed
- logging.debug(this, s"received active ack for '$aid' which has no entry")(tid)
- }
- }
+ override def totalActiveActivations = loadBalancerData.totalActivationCount
- /**
- * Creates an activation entry and insert into various maps.
- */
- private def setupActivation(action: ExecutableWhiskAction, activationId: ActivationId, namespaceId: UUID, invokerName: InstanceId, transid: TransactionId): ActivationEntry = {
- val timeout = action.limits.timeout.duration + activeAckTimeoutGrace
- // Install a timeout handler for the catastrophic case where an active ack is not received at all
- // (because say an invoker is down completely, or the connection to the message bus is disrupted) or when
- // the active ack is significantly delayed (possibly dues to long queues but the subject should not be penalized);
- // in this case, if the activation handler is still registered, remove it and update the books.
- loadBalancerData.putActivation(activationId, {
- actorSystem.scheduler.scheduleOnce(timeout) {
- processCompletion(Left(activationId), transid, forced = true)
- }
-
- ActivationEntry(activationId, namespaceId, invokerName, Promise[Either[ActivationId, WhiskActivation]]())
- })
+ override def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(
+ implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] = {
+ chooseInvoker(msg.user, action).flatMap { invokerName =>
+ val entry = setupActivation(action, msg.activationId, msg.user.uuid, invokerName, transid)
+ sendActivationToInvoker(messageProducer, msg, invokerName).map { _ =>
+ entry.promise.future
+ }
}
-
- /**
- * Creates or updates a health test action by updating the entity store.
- * This method is intended for use on startup.
- * @return Future that completes successfully iff the action is added to the database
- */
- private def createTestActionForInvokerHealth(db: EntityStore, action: WhiskAction): Future[Unit] = {
- implicit val tid = TransactionId.loadbalancer
- WhiskAction.get(db, action.docid).flatMap { oldAction =>
- WhiskAction.put(db, action.revision(oldAction.rev))(tid, notifier = None)
- }.recover {
- case _: NoDocumentException => WhiskAction.put(db, action)(tid, notifier = None)
- }.map(_ => {}).andThen {
- case Success(_) => logging.info(this, "test action for invoker health now exists")
- case Failure(e) => logging.error(this, s"error creating test action for invoker health: $e")
+ }
+
+ /** An indexed sequence of all invokers in the current system */
+ def allInvokers: Future[IndexedSeq[(InstanceId, InvokerState)]] =
+ invokerPool
+ .ask(GetStatus)(Timeout(5.seconds))
+ .mapTo[IndexedSeq[(InstanceId, InvokerState)]]
+
+ /**
+ * Tries to fill in the result slot (i.e., complete the promise) when a completion message arrives.
+ * The promise is removed form the map when the result arrives or upon timeout.
+ *
+ * @param msg is the kafka message payload as Json
+ */
+ private def processCompletion(response: Either[ActivationId, WhiskActivation],
+ tid: TransactionId,
+ forced: Boolean): Unit = {
+ val aid = response.fold(l => l, r => r.activationId)
+ loadBalancerData.removeActivation(aid) match {
+ case Some(entry) =>
+ logging.info(this, s"${if (!forced) "received" else "forced"} active ack for '$aid'")(tid)
+ if (!forced) {
+ entry.promise.trySuccess(response)
+ } else {
+ entry.promise.tryFailure(new Throwable("no active ack received"))
}
+ case None =>
+ // the entry was already removed
+ logging.debug(this, s"received active ack for '$aid' which has no entry")(tid)
}
-
- /** Gets a producer which can publish messages to the kafka bus. */
- private val messagingProvider = SpiLoader.get[MessagingProvider]
- private val messageProducer = messagingProvider.getProducer(config, executionContext)
-
- private def sendActivationToInvoker(producer: MessageProducer, msg: ActivationMessage, invoker: InstanceId): Future[RecordMetadata] = {
- imp |