You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2018/02/06 15:42:17 UTC

[GitHub] dubeejw closed pull request #3187: reduce rule activation records

dubeejw closed pull request #3187: reduce rule activation records
URL: https://github.com/apache/incubator-openwhisk/pull/3187
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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 b6e635a22c..fa107a6bde 100644
--- a/common/scala/src/main/scala/whisk/core/entity/DocInfo.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/DocInfo.scala
@@ -37,7 +37,7 @@ import whisk.core.entity.ArgNormalizer.trim
  *
  * @param id the document id, required not null
  */
-protected[core] class DocId private (val id: String) extends AnyVal {
+protected[core] class DocId(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)
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 a78045d4cb..c52af75284 100644
--- a/common/scala/src/main/scala/whisk/core/entity/FullyQualifiedEntityName.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/FullyQualifiedEntityName.scala
@@ -48,7 +48,7 @@ protected[core] case class FullyQualifiedEntityName(path: EntityPath, name: Enti
    */
   def add(n: EntityName) = FullyQualifiedEntityName(path.addPath(name), n)
 
-  def toDocId = DocId(qualifiedName)
+  def toDocId = new DocId(qualifiedName)
   def namespace: EntityName = path.root
   def qualifiedNameWithLeadingSlash: String = EntityPath.PATHSEP + qualifiedName
   def asString = path.addPath(name) + version.map("@" + _.toString).getOrElse("")
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 044d5a25fb..f50fdb1db5 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
@@ -19,17 +19,22 @@ package whisk.core.controller
 
 import java.time.{Clock, Instant}
 
+import scala.collection.immutable.Map
+import scala.concurrent.Future
+import scala.util.{Failure, Success}
+
 import akka.actor.ActorSystem
 import akka.http.scaladsl.Http
 import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
 import akka.http.scaladsl.model.HttpMethods.POST
-import akka.http.scaladsl.model.StatusCodes._
-import akka.http.scaladsl.model.{HttpEntity, HttpRequest, MediaTypes, Uri}
+import akka.http.scaladsl.model.StatusCodes.{Accepted, BadRequest, InternalServerError, OK}
 import akka.http.scaladsl.model.Uri.Path
 import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
+import akka.http.scaladsl.model._
 import akka.http.scaladsl.server.{RequestContext, RouteResult}
 import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
 import akka.stream.ActorMaterializer
+import spray.json.DefaultJsonProtocol._
 import spray.json._
 import whisk.common.TransactionId
 import whisk.core.controller.RestApiCommons.ListLimit
@@ -37,8 +42,7 @@ import whisk.core.database.CacheChangeNotification
 import whisk.core.entitlement.Collection
 import whisk.core.entity._
 import whisk.core.entity.types.{ActivationStore, EntityStore}
-
-import scala.concurrent.Future
+import whisk.http.ErrorResponse
 
 /** A trait implementing the triggers API. */
 trait WhiskTriggersApi extends WhiskCollectionAPI {
@@ -59,10 +63,9 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
   protected val activationStore: ActivationStore
 
   /** JSON response formatter. */
-  import RestApiCommons.jsonDefaultResponsePrinter
-
   /** Path to Triggers REST API. */
   protected val triggersPath = "triggers"
+  protected val url = Uri(s"http://localhost:${whiskConfig.servicePort}")
 
   protected implicit val materializer: ActorMaterializer
 
@@ -107,10 +110,8 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
     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,
@@ -122,70 +123,42 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
             version = trigger.version,
             duration = None)
 
-          logging.debug(this, s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
-          WhiskActivation.put(activationStore, triggerActivation) recover {
-            case t =>
-              logging.error(this, s"[POST] storing trigger activation $triggerActivationId failed: ${t.getMessage}")
-          }
-
-          val url = Uri(s"http://localhost:${whiskConfig.servicePort}")
-
-          trigger.rules.map {
-            _.filter {
-              case (ruleName, rule) => rule.status == Status.ACTIVE
-            } foreach {
-              case (ruleName, rule) =>
-                val ruleActivationId = activationIdFactory.make()
-                val ruleActivation = WhiskActivation(
-                  namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
-                  ruleName.name,
-                  user.subject,
-                  ruleActivationId,
-                  Instant.now(Clock.systemUTC()),
-                  Instant.EPOCH,
-                  cause = Some(triggerActivationId),
-                  response = ActivationResponse.success(),
-                  version = trigger.version,
-                  duration = None)
-                WhiskActivation.put(activationStore, ruleActivation) recover {
-                  case t =>
-                    logging.error(this, s"[POST] storing rule activation $ruleActivationId failed: ${t.getMessage}")
-                }
-
-                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
+          // List of active rules associated with the trigger
+          val activeRules: Map[FullyQualifiedEntityName, ReducedRule] =
+            trigger.rules.map(_.filter(_._2.status == Status.ACTIVE)).getOrElse(Map.empty)
+
+          if (activeRules.nonEmpty) {
+            val args: JsObject = trigger.parameters.merge(payload).getOrElse(JsObject())
+            val actionLogList: Iterable[Future[JsObject]] = activateRules(user, args, activeRules)
+
+            // For each of the action activation results, generate a log message to attach to the trigger activation
+            Future
+              .sequence(actionLogList)
+              .map(_.map(_.compactPrint))
+              .onComplete {
+                case Success(triggerLogs) =>
+                  val triggerActivationDoc = triggerActivation.withLogs(ActivationLogs(triggerLogs.toVector))
+                  logging
+                    .debug(
+                      this,
+                      s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
+                  WhiskActivation.put(activationStore, triggerActivationDoc) recover {
+                    case t =>
+                      logging
+                        .error(this, s"[POST] storing trigger activation $triggerActivationId failed: ${t.getMessage}")
                   }
-                }.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.debug(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")
-                        }
-                    }
-                }
-            }
+                case Failure(e) =>
+                  logging.error(this, s"Failed to write action activation results to trigger activation: $e")
+                  logging
+                    .info(
+                      this,
+                      s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
+                  WhiskActivation.put(activationStore, triggerActivation) recover {
+                    case t =>
+                      logging
+                        .error(this, s"[POST] storing trigger activation $triggerActivationId failed: ${t.getMessage}")
+                  }
+              }
           }
 
           complete(Accepted, triggerActivationId.toJsObject)
@@ -326,6 +299,132 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
     complete(OK, trigger.withoutRules)
   }
 
+  /**
+   * Iterates through each active rule and invoke each mapped action.
+   */
+  private def activateRules(user: Identity,
+                            args: JsObject,
+                            rulesToActivate: Map[FullyQualifiedEntityName, ReducedRule])(
+    implicit transid: TransactionId): Iterable[Future[JsObject]] = {
+    rulesToActivate.map {
+      case (ruleName, rule) =>
+        // Invoke the action. Retain action results for inclusion in the trigger activation record
+        val actionActivationResult: Future[JsObject] = postActivation(user, rule, args)
+          .flatMap { response =>
+            response.status match {
+              case OK | Accepted =>
+                Unmarshal(response.entity).to[JsObject].map { activationResponse =>
+                  val activationId: JsValue = activationResponse.fields("activationId")
+                  logging.debug(this, s"trigger-fired action '${rule.action}' invoked with activation $activationId")
+                  ruleResult(ActivationResponse.Success, ruleName, rule.action, Some(activationId))
+                }
+
+              // all proper controller responses are JSON objects that deserialize to an ErrorResponse instance
+              case code if (response.entity.contentType == ContentTypes.`application/json`) =>
+                Unmarshal(response.entity).to[ErrorResponse].map { e =>
+                  val statusCode =
+                    if (code != InternalServerError) {
+                      logging
+                        .debug(
+                          this,
+                          s"trigger-fired action '${rule.action}' failed to invoke with ${e.error}, ${e.code}")
+                      ActivationResponse.ApplicationError
+                    } else {
+                      logging
+                        .error(
+                          this,
+                          s"trigger-fired action '${rule.action}' failed to invoke with ${e.error}, ${e.code}")
+                      ActivationResponse.WhiskError
+                    }
+                  ruleResult(statusCode, ruleName, rule.action, errorMsg = Some(e.error))
+                }
+
+              case code =>
+                logging.error(this, s"trigger-fired action '${rule.action}' failed to invoke with status code $code")
+                Unmarshal(response.entity).to[String].map { error =>
+                  ruleResult(ActivationResponse.WhiskError, ruleName, rule.action, errorMsg = Some(error))
+                }
+            }
+          }
+          .recover {
+            case t =>
+              logging.error(this, s"trigger-fired action '${rule.action}' failed to invoke with $t")
+              ruleResult(
+                ActivationResponse.WhiskError,
+                ruleName,
+                rule.action,
+                errorMsg = Some(InternalServerError.defaultMessage))
+          }
+
+        actionActivationResult
+    }
+  }
+
+  /**
+   * Posts an action activation. Currently done by posting internally to the controller.
+   * TODO: use a poper path that does not route through HTTP.
+   *
+   * @param rule the name of the rule that is activated
+   * @param args the arguments to post to the action
+   * @return a future with the HTTP response from the action activation
+   */
+  private def postActivation(user: Identity, rule: ReducedRule, args: JsObject): Future[HttpResponse] = {
+    // Build the url to invoke an action mapped to the rule
+    val actionUrl = baseControllerPath / rule.action.path.root.asString / "actions"
+
+    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 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.compactPrint))
+
+    Http().singleRequest(request)
+  }
+
+  /**
+   * Create JSON object containing the pertinent rule activation details.
+   * {
+   *   "rule": "my-rule",
+   *   "action": "my-action",
+   *   "statusCode": 0,
+   *   "status": "success",
+   *   "activationId": "...",                              // either this field, ...
+   *   "error": "The requested resource does not exist."   // ... or this field will be present
+   * }
+   *
+   * @param statusCode one of ActivationResponse values
+   * @param ruleName the name of the rule that was activated
+   * @param actionName the name of the action activated by the rule
+   * @param actionActivationId the activation id, if there is one
+   * @param errorMsg the rror messages otherwise
+   * @return JsObject as formatted above
+   */
+  private def ruleResult(statusCode: Int,
+                         ruleName: FullyQualifiedEntityName,
+                         actionName: FullyQualifiedEntityName,
+                         actionActivationId: Option[JsValue] = None,
+                         errorMsg: Option[String] = None): JsObject = {
+    JsObject(
+      Map(
+        "rule" -> JsString(ruleName.asString),
+        "action" -> JsString(actionName.asString),
+        "statusCode" -> JsNumber(statusCode),
+        "success" -> JsBoolean(statusCode == ActivationResponse.Success)) ++
+        actionActivationId.map("activationId" -> _.toJson) ++
+        errorMsg.map("error" -> JsString(_)))
+  }
+
+  /** Common base bath for the controller, used by internal action activation mechanism. */
+  private val baseControllerPath = Path("/api/v1/namespaces")
+
   /** Custom unmarshaller for query parameters "limit" for "list" operations. */
   private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection)
 }
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 9302f4c9a9..2e9d6b0644 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
@@ -159,7 +159,7 @@ protected[actions] trait PrimitiveActions {
     // 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 docid = new DocId(WhiskEntity.qualifiedName(user.namespace.toPath, activationId))
     val (promise, finisher) = ActivationFinisher.props({ () =>
       WhiskActivation.get(activationStore, docid)
     })
diff --git a/tests/src/test/scala/system/basic/WskBasicTests.scala b/tests/src/test/scala/system/basic/WskBasicTests.scala
index 1696fee642..8e68d29e59 100644
--- a/tests/src/test/scala/system/basic/WskBasicTests.scala
+++ b/tests/src/test/scala/system/basic/WskBasicTests.scala
@@ -47,6 +47,11 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   val wsk: common.rest.WskRest = new WskRest
   val defaultAction: Some[String] = Some(TestUtils.getTestActionFilename("hello.js"))
 
+  /**
+   * Append the current timestamp in ms
+   */
+  def withTimestamp(text: String) = s"${text}-${System.currentTimeMillis}"
+
   behavior of "Wsk REST"
 
   it should "reject creating duplicate entity" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -484,26 +489,53 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   behavior of "Wsk Trigger REST"
 
   it should "create, update, get, fire and list trigger" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
-    val name = "listTriggers"
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("t1tor1")
+    val actionName = withTimestamp("a1")
     val params = Map("a" -> "A".toJson)
-    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-      trigger.create(name, parameters = params)
-      trigger.create(name, update = true)
+    val ns = wsk.namespace.whois()
+
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName, parameters = params)
+      trigger.create(triggerName, update = true)
+    }
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+      action.create(name, defaultAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = actionName)
     }
-    val trigger = wsk.trigger.get(name)
+
+    val trigger = wsk.trigger.get(triggerName)
     trigger.getFieldJsValue("parameters") shouldBe JsArray(JsObject("key" -> JsString("a"), "value" -> JsString("A")))
     trigger.getFieldJsValue("publish") shouldBe JsBoolean(false)
     trigger.getField("version") shouldBe "0.0.2"
 
     val dynamicParams = Map("t" -> "T".toJson)
-    val run = wsk.trigger.fire(name, dynamicParams)
+    val run = wsk.trigger.fire(triggerName, dynamicParams)
     withActivation(wsk.activation, run) { activation =>
       activation.response.result shouldBe Some(dynamicParams.toJson)
       activation.duration shouldBe 0L // shouldn't exist but CLI generates it
       activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+      activation.logs shouldBe defined
+      activation.logs.get.size shouldBe 1
+
+      val logEntry = activation.logs.get(0).parseJson.asJsObject
+      val logs = JsArray(logEntry)
+      val ruleActivationId: String = logEntry.getFields("activationId")(0).convertTo[String]
+      val expectedLogs = JsArray(
+        JsObject(
+          "statusCode" -> JsNumber(0),
+          "activationId" -> JsString(ruleActivationId),
+          "success" -> JsBoolean(true),
+          "rule" -> JsString(ns + "/" + ruleName),
+          "action" -> JsString(ns + "/" + actionName)))
+      logs shouldBe expectedLogs
     }
 
-    val runWithNoParams = wsk.trigger.fire(name, Map())
+    val runWithNoParams = wsk.trigger.fire(triggerName, Map())
     withActivation(wsk.activation, runWithNoParams) { activation =>
       activation.response.result shouldBe Some(JsObject())
       activation.duration shouldBe 0L // shouldn't exist but CLI generates it
@@ -512,7 +544,7 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
 
     val triggerList = wsk.trigger.list()
     val triggers = triggerList.getBodyListJsObject()
-    triggers.exists(trigger => RestResult.getField(trigger, "name") == name) shouldBe true
+    triggers.exists(trigger => RestResult.getField(trigger, "name") == triggerName) shouldBe true
   }
 
   it should "create, and get a trigger summary" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -554,17 +586,26 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   }
 
   it should "create, and fire a trigger using a parameter file" in withAssetCleaner(wskprops) {
-    val name = "paramFileTrigger"
-    val file = Some(TestUtils.getTestActionFilename("argCheck.js"))
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("paramFileTrigger")
+    val actionName = withTimestamp("a1")
     val argInput = Some(TestUtils.getTestActionFilename("validInput2.json"))
 
     (wp, assetHelper) =>
-      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-        trigger.create(name)
+      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+        trigger.create(triggerName)
+      }
+
+      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+        action.create(name, defaultAction)
+      }
+
+      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+        rule.create(name, trigger = triggerName, action = actionName)
       }
 
       val expectedOutput = JsObject("payload" -> JsString("test"))
-      val run = wsk.trigger.fire(name, parameterFile = argInput)
+      val run = wsk.trigger.fire(triggerName, parameterFile = argInput)
       withActivation(wsk.activation, run) { activation =>
         activation.response.result shouldBe Some(expectedOutput)
       }
@@ -596,12 +637,23 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   }
 
   it should "create, and fire a trigger to ensure result is empty" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
-    val name = "emptyResultTrigger"
-    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-      trigger.create(name)
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("emptyResultTrigger")
+    val actionName = withTimestamp("a1")
+
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName)
+    }
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+      action.create(name, defaultAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = actionName)
     }
 
-    val run = wsk.trigger.fire(name)
+    val run = wsk.trigger.fire(triggerName)
     withActivation(wsk.activation, run) { activation =>
       activation.response.result shouldBe Some(JsObject())
     }
@@ -637,6 +689,65 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
     stderr should include regex ("""The requested resource does not exist.""")
   }
 
+  it should "create and fire a trigger with a rule whose action has been deleted" in withAssetCleaner(wskprops) {
+    (wp, assetHelper) =>
+      val ruleName1 = withTimestamp("r1toa1")
+      val ruleName2 = withTimestamp("r2toa2")
+      val triggerName = withTimestamp("t1tor1r2")
+      val actionName1 = withTimestamp("a1")
+      val actionName2 = withTimestamp("a2")
+      val ns = wsk.namespace.whois()
+
+      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+        trigger.create(triggerName)
+        trigger.create(triggerName, update = true)
+      }
+
+      assetHelper.withCleaner(wsk.action, actionName1) { (action, name) =>
+        action.create(name, defaultAction)
+      }
+      wsk.action.create(actionName2, defaultAction) // Delete this after the rule is created
+
+      assetHelper.withCleaner(wsk.rule, ruleName1) { (rule, name) =>
+        rule.create(name, trigger = triggerName, action = actionName1)
+      }
+      assetHelper.withCleaner(wsk.rule, ruleName2) { (rule, name) =>
+        rule.create(name, trigger = triggerName, action = actionName2)
+      }
+      wsk.action.delete(actionName2)
+
+      val run = wsk.trigger.fire(triggerName)
+      withActivation(wsk.activation, run) { activation =>
+        activation.duration shouldBe 0L // shouldn't exist but CLI generates it
+        activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+        activation.logs shouldBe defined
+        activation.logs.get.size shouldBe 2
+
+        val logEntry1 = activation.logs.get(0).parseJson.asJsObject
+        val logEntry2 = activation.logs.get(1).parseJson.asJsObject
+        val logs = JsArray(logEntry1, logEntry2)
+        val ruleActivationId: String = if (logEntry1.getFields("activationId").size == 1) {
+          logEntry1.getFields("activationId")(0).convertTo[String]
+        } else {
+          logEntry2.getFields("activationId")(0).convertTo[String]
+        }
+        val expectedLogs = JsArray(
+          JsObject(
+            "statusCode" -> JsNumber(0),
+            "activationId" -> JsString(ruleActivationId),
+            "success" -> JsBoolean(true),
+            "rule" -> JsString(ns + "/" + ruleName1),
+            "action" -> JsString(ns + "/" + actionName1)),
+          JsObject(
+            "statusCode" -> JsNumber(1),
+            "success" -> JsBoolean(false),
+            "error" -> JsString("The requested resource does not exist."),
+            "rule" -> JsString(ns + "/" + ruleName2),
+            "action" -> JsString(ns + "/" + actionName2)))
+        logs shouldBe expectedLogs
+      }
+  }
+
   behavior of "Wsk Rule REST"
 
   it should "create rule, get rule, update rule and list rule" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -808,18 +919,28 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
 
   it should "create a trigger, and fire a trigger to get its individual fields from an activation" in withAssetCleaner(
     wskprops) { (wp, assetHelper) =>
-    val name = "activationFields"
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("activationFields")
+    val actionName = withTimestamp("a1")
 
-    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-      trigger.create(name)
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName)
+    }
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+      action.create(name, defaultAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = actionName)
     }
 
     val ns = wsk.namespace.whois()
-    val run = wsk.trigger.fire(name)
+    val run = wsk.trigger.fire(triggerName)
     withActivation(wsk.activation, run) { activation =>
       var result = wsk.activation.get(Some(activation.activationId))
       result.getField("namespace") shouldBe ns
-      result.getField("name") shouldBe name
+      result.getField("name") shouldBe triggerName
       result.getField("version") shouldBe "0.0.1"
       result.getFieldJsValue("publish") shouldBe JsBoolean(false)
       result.getField("subject") shouldBe ns
diff --git a/tests/src/test/scala/system/basic/WskRuleTests.scala b/tests/src/test/scala/system/basic/WskRuleTests.scala
index 2baac6d850..bcb9dea083 100644
--- a/tests/src/test/scala/system/basic/WskRuleTests.scala
+++ b/tests/src/test/scala/system/basic/WskRuleTests.scala
@@ -100,13 +100,11 @@ abstract class WskRuleTests extends TestHelpers with WskTestHelpers {
 
     withActivation(wsk.activation, run) { triggerActivation =>
       triggerActivation.cause shouldBe None
-
-      withActivationsFromEntity(
-        wsk.activation,
-        ruleName,
-        since = Some(triggerActivation.start.minusMillis(activationTimeSkewFactorMs))) {
-        _.head.cause shouldBe Some(triggerActivation.activationId)
-      }
+      triggerActivation.logs.get.size shouldBe (1)
+      val logs = triggerActivation.logs.get.mkString(" ")
+      logs should include(""""statusCode":0""")
+      logs should include(""""activationId":""")
+      logs should include(""""success":true""")
 
       withActivationsFromEntity(
         wsk.activation,
@@ -137,13 +135,11 @@ abstract class WskRuleTests extends TestHelpers with WskTestHelpers {
 
     withActivation(wsk.activation, run) { triggerActivation =>
       triggerActivation.cause shouldBe None
-
-      withActivationsFromEntity(
-        wsk.activation,
-        ruleName,
-        since = Some(triggerActivation.start.minusMillis(activationTimeSkewFactorMs))) {
-        _.head.cause shouldBe Some(triggerActivation.activationId)
-      }
+      triggerActivation.logs.get.size shouldBe (1)
+      val logs = triggerActivation.logs.get.mkString(" ")
+      logs should include(""""statusCode":0""")
+      logs should include(""""activationId":""")
+      logs should include(""""success":true""")
 
       withActivationsFromEntity(
         wsk.activation,
@@ -177,13 +173,11 @@ abstract class WskRuleTests extends TestHelpers with WskTestHelpers {
 
     withActivation(wsk.activation, run) { triggerActivation =>
       triggerActivation.cause shouldBe None
-
-      withActivationsFromEntity(
-        wsk.activation,
-        ruleName,
-        since = Some(triggerActivation.start.minusMillis(activationTimeSkewFactorMs))) {
-        _.head.cause shouldBe Some(triggerActivation.activationId)
-      }
+      triggerActivation.logs.get.size shouldBe (1)
+      val logs = triggerActivation.logs.get.mkString(" ")
+      logs should include(""""statusCode":0""")
+      logs should include(""""activationId":""")
+      logs should include(""""success":true""")
 
       withActivationsFromEntity(
         wsk.activation,
diff --git a/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala b/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala
index a00b1ea6e4..1dc19f0cfb 100644
--- a/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala
+++ b/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala
@@ -37,6 +37,7 @@ class Swift311Tests extends TestHelpers with WskTestHelpers with Matchers {
   val wsk = new WskRest
   val expectedDuration = 45 seconds
   val activationPollDuration = 60 seconds
+  val defaultJsAction = Some(TestUtils.getTestActionFilename("hello.js"))
 
   lazy val runtimeContainer = "swift:3.1.1"
 
@@ -116,10 +117,20 @@ class Swift311Tests extends TestHelpers with WskTestHelpers with Matchers {
   it should "allow Swift actions to trigger events" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
     // create a trigger
     val triggerName = s"TestTrigger ${System.currentTimeMillis()}"
+    val ruleName = s"TestTriggerRule ${System.currentTimeMillis()}"
+    val ruleActionName = s"TestTriggerAction ${System.currentTimeMillis()}"
     assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
       trigger.create(triggerName)
     }
 
+    assetHelper.withCleaner(wsk.action, ruleActionName) { (action, name) =>
+      action.create(name, defaultJsAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = ruleActionName)
+    }
+
     // create an action that fires the trigger
     val file = TestUtils.getTestActionFilename("trigger.swift")
     val actionName = "ActionThatTriggers"
diff --git a/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala b/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala
index 200086f6da..3a8d3cee4f 100644
--- a/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala
@@ -64,6 +64,7 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
   val namespace = EntityPath(creds.subject.asString)
   val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
   def aname() = MakeName.next("triggers_tests")
+  def afullname(namespace: EntityPath, name: String) = FullyQualifiedEntityName(namespace, EntityName(name))
   val parametersLimit = Parameters.sizeLimit
 
   //// GET /triggers
@@ -319,9 +320,13 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
   //// POST /triggers/name
   it should "fire a trigger" in {
     implicit val tid = transid()
-    val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
+    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, "bogus action"))
+    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {
+      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))
+    })
     val content = JsObject("xxx" -> "yyy".toJson)
     put(entityStore, trigger)
+    put(entityStore, rule)
     Post(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
       status should be(Accepted)
       val response = responseAs[JsObject]
@@ -342,8 +347,12 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
 
   it should "fire a trigger without args" in {
     implicit val tid = transid()
-    val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
+    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, "bogus action"))
+    val trigger = WhiskTrigger(namespace, rule.trigger.name, Parameters("x", "b"), rules = Some {
+      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))
+    })
     put(entityStore, trigger)
+    put(entityStore, rule)
     Post(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
       val response = responseAs[JsObject]
       val JsString(id) = response.fields("activationId")


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services