You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by cs...@apache.org on 2017/11/22 22:35:51 UTC

[incubator-openwhisk-package-alarms] branch master updated: Add interval support (#113)

This is an automated email from the ASF dual-hosted git repository.

csantanapr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk-package-alarms.git


The following commit(s) were added to refs/heads/master by this push:
     new e828151  Add interval support (#113)
e828151 is described below

commit e8281517dd53194335205994061cba8e7fd32828
Author: Jason Peterson <ja...@us.ibm.com>
AuthorDate: Wed Nov 22 17:35:50 2017 -0500

    Add interval support (#113)
    
    * add interval support
    
    * add documentation for the interval trigger feed
    
    * add tests for interval trigger feed
---
 README.md                                          | 32 +++++++++-
 action/alarmWebAction.js                           | 70 +++++++++++++++-------
 action/lib/Database.js                             |  6 +-
 installCatalog.sh                                  |  6 ++
 provider/lib/cronAlarm.js                          |  1 +
 provider/lib/dateAlarm.js                          |  1 +
 provider/lib/intervalAlarm.js                      | 66 ++++++++++++++++++++
 provider/lib/utils.js                              | 51 ++++++++++++----
 .../system/health/AlarmsHealthFeedTests.scala      | 43 ++++++++++++-
 .../scala/system/packages/AlarmsFeedTests.scala    | 59 ++++++++++++++++++
 .../scala/system/packages/AlarmsFeedWebTests.scala |  8 +--
 11 files changed, 301 insertions(+), 42 deletions(-)

diff --git a/README.md b/README.md
index 33f383c..2b0d0d5 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,10 @@ The package includes the following feeds.
 | `/whisk.system/alarms` | package | - | Alarms and periodic utility |
 | `/whisk.system/alarms/alarm` | feed | cron, trigger_payload, maxTriggers, startDate, stopDate | Fire trigger event periodically |
 | `/whisk.system/alarms/once` | feed | date, trigger_payload | Fire trigger event once on a specific date |
+| `/whisk.system/alarms/interval` | feed | minutes, trigger_payload, startDate, stopDate | Fire trigger event on an interval based schedule |
 
 
-## Firing a trigger event periodically
+## Firing a trigger event periodically on a time based schedule
 
 The `/whisk.system/alarms/alarm` feed configures the Alarm service to fire a trigger event at a specified frequency. The parameters are as follows:
 
@@ -57,6 +58,35 @@ January 1, 2019, 00:00:00 UTC and will stop firing January 31, 2019, 23:59:00 UT
 
 Each generated event will include as parameters the properties specified in the `trigger_payload` value. In this case, each trigger event will have parameters `name=Odin` and `place=Asgard`.
 
+
+## Firing a trigger event periodically on an interval based schedule
+
+The `/whisk.system/alarms/interval` feed configures the Alarm service to fire a trigger event on an interval based schedule. The parameters are as follows:
+
+- `minutes`: An integer representing the length of the interval (in minutes) between trigger fires.
+
+- `trigger_payload`: The value of this parameter becomes the content of the trigger every time the trigger is fired.
+
+- `startDate`: The date when the first trigger will be fired.  Subsequent fires will occur based on the interval length specified by the `minutes` parameter.   
+
+- `stopDate`: The date when the trigger will stop running.  Triggers will no longer be fired once this date has been reached.
+
+  **Note**: The `startDate` and `stopDate` parameters support an integer or string value.  The integer value represents the number of milliseconds 
+  since 1 January 1970 00:00:00 UTC and the string value should be in the ISO 8601 format (http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15).
+
+
+The following is an example of creating a trigger that will be fired once every 90 minutes.  The trigger will not start firing until
+January 1, 2019, 00:00:00 UTC and will stop firing January 31, 2019, 23:59:00 UTC.
+
+  ```
+  wsk trigger create interval \
+    --feed /whisk.system/alarms/interval \
+    --param minutes 90 \
+    --param trigger_payload "{\"name\":\"Odin\",\"place\":\"Asgard\"}" \
+    --param startDate "2019-01-01T00:00:00.000Z" \
+    --param stopDate "2019-01-31T23:59:00.000Z"
+  ```
+
 ## Firing a trigger event once  
 
 The `/whisk.system/alarms/once` feed configures the Alarm service to fire a trigger event on a specified date. The parameters are as follows:
diff --git a/action/alarmWebAction.js b/action/alarmWebAction.js
index 9f8789c..7c620c6 100644
--- a/action/alarmWebAction.js
+++ b/action/alarmWebAction.js
@@ -49,16 +49,33 @@ function main(params) {
             newTrigger.date = date;
         }
         else {
-            if (!params.cron) {
-                return common.sendError(400, 'alarms trigger feed is missing the cron parameter');
+            var cronHandle;
+
+            if (params.isInterval) {
+                if (!params.minutes) {
+                    return common.sendError(400, 'interval trigger feed is missing the minutes parameter');
+                }
+                if (+params.minutes !== parseInt(params.minutes)) {
+                    return common.sendError(400, 'the minutes parameter must be an integer');
+                }
+                var minutesParam = parseInt(params.minutes);
+
+                if (minutesParam <= 0) {
+                    return common.sendError(400, 'the minutes parameter must be an integer greater than zero');
+                }
+                newTrigger.minutes = minutesParam;
             }
+            else {
+                if (!params.cron) {
+                    return common.sendError(400, 'alarms trigger feed is missing the cron parameter');
+                }
 
-            var cronHandle;
-            try {
-                cronHandle = new CronJob(params.cron, function() {});
-                newTrigger.cron = params.cron;
-            } catch(ex) {
-                return common.sendError(400, `cron pattern '${params.cron}' is not valid`);
+                try {
+                    cronHandle = new CronJob(params.cron, function() {});
+                    newTrigger.cron = params.cron;
+                } catch(ex) {
+                    return common.sendError(400, `cron pattern '${params.cron}' is not valid`);
+                }
             }
 
             if (params.startDate) {
@@ -68,23 +85,31 @@ function main(params) {
                 }
                 newTrigger.startDate = startDate;
             }
+            else if (params.isInterval) {
+                //if startDate was not given we will start it 30 seconds
+                //from now since startDate must be in the future
+                newTrigger.startDate = Date.now() + (1000 * 30);
+            }
 
-            if (params.stopDate) {
-                if (params.maxTriggers) {
+            if (params.maxTriggers && params.stopDate) {
+                if (params.isInterval) {
+                    return common.sendError(400, 'maxTriggers is not supported for the interval trigger feed');
+                }
+                else {
                     return common.sendError(400, 'maxTriggers is not allowed when the stopDate parameter is specified');
                 }
-
-                var stopDate = validateDate(params.stopDate, 'stopDate', params.startDate);
+            }
+            else if (params.stopDate) {
+                var stopDate = validateDate(params.stopDate, 'stopDate', newTrigger.startDate);
                 if (stopDate !== params.stopDate) {
                     return common.sendError(400, stopDate);
                 }
-                //verify that the next scheduled trigger fire will occur before the stop date
-                var triggerDate = cronHandle.nextDate();
-                if (triggerDate.isAfter(new Date(params.stopDate))) {
-                    return common.sendError(400, 'the next scheduled trigger fire is not until after the stop date');
-                }
-
                 newTrigger.stopDate = stopDate;
+
+                //verify that the first scheduled trigger fire will occur before the stop date
+                if (cronHandle && cronHandle.nextDate().isAfter(new Date(params.stopDate))) {
+                    return common.sendError(400, 'the first scheduled trigger fire is not until after the stop date');
+                }
             }
         }
 
@@ -137,9 +162,14 @@ function main(params) {
                     body.config.date = doc.date;
                 }
                 else {
-                    body.config.cron = doc.cron;
                     body.config.startDate = doc.startDate;
                     body.config.stopDate = doc.stopDate;
+                    if (doc.minutes) {
+                        body.config.minutes = doc.minutes;
+                    }
+                    else {
+                        body.config.cron = doc.cron;
+                    }
                 }
                 resolve({
                     statusCode: 200,
@@ -158,7 +188,7 @@ function main(params) {
             common.verifyTriggerAuth(triggerURL, params.authKey, true)
             .then(() => {
                 db = new Database(params.DB_URL, params.DB_NAME);
-                return db.updateTrigger(triggerID, 0);
+                return db.disableTrigger(triggerID, 0);
             })
             .then(id => {
                 return db.deleteTrigger(id, 0);
diff --git a/action/lib/Database.js b/action/lib/Database.js
index e1fdb27..a878b85 100644
--- a/action/lib/Database.js
+++ b/action/lib/Database.js
@@ -85,7 +85,7 @@ module.exports = function(dbURL, dbName) {
         });
     };
 
-    this.updateTrigger = function(triggerID, retryCount) {
+    this.disableTrigger = function(triggerID, retryCount) {
 
         return new Promise(function(resolve, reject) {
 
@@ -98,7 +98,7 @@ module.exports = function(dbURL, dbName) {
                         if (err) {
                             if (err.statusCode === 409 && retryCount < 5) {
                                 setTimeout(function () {
-                                    utilsDB.updateTrigger(triggerID, (retryCount + 1))
+                                    utilsDB.disableTrigger(triggerID, (retryCount + 1))
                                     .then(id => {
                                         resolve(id);
                                     })
@@ -121,7 +121,7 @@ module.exports = function(dbURL, dbName) {
                     if (retryCount === 0) {
                         var parts = triggerID.split('/');
                         var id = parts[0] + '/_/' + parts[2];
-                        utilsDB.updateTrigger(id, (retryCount + 1))
+                        utilsDB.disableTrigger(id, (retryCount + 1))
                         .then(id => {
                             resolve(id);
                         })
diff --git a/installCatalog.sh b/installCatalog.sh
index 3b34e49..ddc9337 100755
--- a/installCatalog.sh
+++ b/installCatalog.sh
@@ -76,6 +76,12 @@ $WSK_CLI -i --apihost "$EDGEHOST" action update --kind nodejs:6 --auth "$AUTH" a
      -a feed true \
      -p fireOnce true
 
+$WSK_CLI -i --apihost "$EDGEHOST" action update --kind nodejs:6 --auth "$AUTH" alarms/interval "$PACKAGE_HOME/action/alarmFeed.zip" \
+     -a description 'Fire trigger at specified interval' \
+     -a parameters '[ {"name":"minutes", "required":true}, {"name":"startDate", "required":false}, {"name":"stopDate", "required":false} ]' \
+     -a feed true \
+     -p isInterval true
+
 if [ -n "$WORKERS" ];
 then
     $WSK_CLI -i --apihost "$EDGEHOST" package update --auth "$AUTH" --shared no alarmsWeb \
diff --git a/provider/lib/cronAlarm.js b/provider/lib/cronAlarm.js
index 98b691b..f60ec62 100644
--- a/provider/lib/cronAlarm.js
+++ b/provider/lib/cronAlarm.js
@@ -10,6 +10,7 @@ module.exports = function(logger, newTrigger) {
         apikey: newTrigger.apikey,
         name: newTrigger.name,
         namespace: newTrigger.namespace,
+        payload: newTrigger.payload,
         cron: newTrigger.cron
     };
 
diff --git a/provider/lib/dateAlarm.js b/provider/lib/dateAlarm.js
index 04d2ec0..011261d 100644
--- a/provider/lib/dateAlarm.js
+++ b/provider/lib/dateAlarm.js
@@ -6,6 +6,7 @@ module.exports = function(logger, newTrigger) {
         apikey: newTrigger.apikey,
         name: newTrigger.name,
         namespace: newTrigger.namespace,
+        payload: newTrigger.payload,
         date: newTrigger.date
     };
 
diff --git a/provider/lib/intervalAlarm.js b/provider/lib/intervalAlarm.js
new file mode 100644
index 0000000..077a68b
--- /dev/null
+++ b/provider/lib/intervalAlarm.js
@@ -0,0 +1,66 @@
+var lt =  require('long-timeout');
+
+module.exports = function(logger, newTrigger) {
+
+
+    var cachedTrigger = {
+        apikey: newTrigger.apikey,
+        name: newTrigger.name,
+        namespace: newTrigger.namespace,
+        payload: newTrigger.payload,
+        minutes: newTrigger.minutes
+    };
+
+    this.scheduleAlarm = function(triggerIdentifier, callback) {
+        var method = 'scheduleIntervalAlarm';
+
+        try {
+            return new Promise(function(resolve, reject) {
+
+                var intervalInMilliSeconds = newTrigger.minutes * 1000 * 60;
+                var startDate = new Date(newTrigger.startDate).getTime();
+
+                if (newTrigger.stopDate) {
+                    cachedTrigger.stopDate = newTrigger.stopDate;
+                    //do not create trigger if the stopDate is in the past
+                    if (new Date(newTrigger.stopDate).getTime() <= Date.now()) {
+                        return reject('the stop date has expired');
+                    }
+                }
+
+                if (startDate > Date.now()) {
+                    //fire the trigger and start the interval on the start date
+                    logger.info(method, triggerIdentifier, 'waiting for start date', startDate);
+                    lt.setTimeout(function() {
+                        logger.info(method, triggerIdentifier, 'firing first trigger and starting interval upon reaching start date', startDate);
+                        var intervalHandle = lt.setInterval(callback, intervalInMilliSeconds);
+                        cachedTrigger.intervalHandle = intervalHandle;
+                        resolve(cachedTrigger);
+                    }, startDate - Date.now());
+                }
+                else {
+                    //fire the trigger and start the interval at the next scheduled interval
+                    //as long as the next scheduled interval is not past the stop date
+                    var intervalsFired = Math.floor((Date.now() - startDate)/intervalInMilliSeconds);
+                    var nextScheduledInterval = startDate + (intervalInMilliSeconds * (intervalsFired + 1));
+
+                    if (newTrigger.stopDate && nextScheduledInterval > new Date(newTrigger.stopDate).getTime()) {
+                        return reject('the next scheduled trigger fire is after the stop date');
+                    }
+
+                    logger.info(method, triggerIdentifier, 'waiting for next interval');
+                    lt.setTimeout(function() {
+                        logger.info(method, triggerIdentifier, 'firing trigger and starting interval for trigger past its start date');
+                        var intervalHandle = lt.setInterval(callback, intervalInMilliSeconds);
+                        cachedTrigger.intervalHandle = intervalHandle;
+                        resolve(cachedTrigger);
+                    }, nextScheduledInterval - Date.now());
+                }
+
+            });
+        } catch (err) {
+            return Promise.reject(err);
+        }
+    };
+
+};
diff --git a/provider/lib/utils.js b/provider/lib/utils.js
index 8c7040b..a8f3153 100644
--- a/provider/lib/utils.js
+++ b/provider/lib/utils.js
@@ -1,8 +1,10 @@
 var request = require('request');
 var HttpStatus = require('http-status-codes');
+var lt =  require('long-timeout');
 var constants = require('./constants.js');
 var DateAlarm = require('./dateAlarm.js');
 var CronAlarm = require('./cronAlarm.js');
+var IntervalAlarm = require('./intervalAlarm.js');
 
 module.exports = function(
   logger,
@@ -36,7 +38,7 @@ module.exports = function(
                 var triggerHandle = utils.triggers[triggerIdentifier];
                 if (triggerHandle && (!triggerHandle.maxTriggers || triggerHandle.maxTriggers === -1 || triggerHandle.triggersLeft > 0)) {
                     try {
-                        utils.fireTrigger(newTrigger.namespace, newTrigger.name, newTrigger.payload, newTrigger.apikey);
+                        utils.fireTrigger(triggerHandle);
                     } catch (e) {
                         logger.error(method, 'Exception occurred while firing trigger', triggerIdentifier, e);
                     }
@@ -48,6 +50,9 @@ module.exports = function(
         if (newTrigger.date) {
             alarm = new DateAlarm(logger, newTrigger);
         }
+        else if (newTrigger.minutes) {
+            alarm = new IntervalAlarm(logger, newTrigger);
+        }
         else {
             alarm = new CronAlarm(logger, newTrigger);
         }
@@ -55,17 +60,16 @@ module.exports = function(
         return alarm.scheduleAlarm(triggerIdentifier, callback);
     };
 
-    this.fireTrigger = function(namespace, name, payload, apikey) {
+    this.fireTrigger = function(dataTrigger) {
         var method = 'fireTrigger';
 
-        var triggerIdentifier = utils.getTriggerIdentifier(apikey, namespace, name);
+        var triggerIdentifier = utils.getTriggerIdentifier(dataTrigger.apikey, dataTrigger.namespace, dataTrigger.name);
         var host = 'https://' + utils.routerHost + ':443';
-        var auth = apikey.split(':');
-        var dataTrigger = utils.triggers[triggerIdentifier];
-        var uri = host + '/api/v1/namespaces/' + namespace + '/triggers/' + name;
+        var auth = dataTrigger.apikey.split(':');
+        var uri = host + '/api/v1/namespaces/' + dataTrigger.namespace + '/triggers/' + dataTrigger.name;
 
-        logger.info(method, 'Cron fired for', triggerIdentifier, 'attempting to fire trigger');
-        utils.postTrigger(dataTrigger, payload, uri, auth, 0)
+        logger.info(method, 'Alarm fired for', triggerIdentifier, 'attempting to fire trigger');
+        utils.postTrigger(dataTrigger, uri, auth, 0)
         .then(triggerId => {
             logger.info(method, 'Trigger', triggerId, 'was successfully fired');
             utils.disableExtinctTriggers(triggerIdentifier, dataTrigger);
@@ -76,7 +80,7 @@ module.exports = function(
         });
     };
 
-    this.postTrigger = function(dataTrigger, payload, uri, auth, retryCount) {
+    this.postTrigger = function(dataTrigger, uri, auth, retryCount) {
         var method = 'postTrigger';
 
         return new Promise(function(resolve, reject) {
@@ -93,7 +97,7 @@ module.exports = function(
                     user: auth[0],
                     pass: auth[1]
                 },
-                json: payload
+                json: dataTrigger.payload
             }, function(error, response) {
                 try {
                     var triggerIdentifier = utils.getTriggerIdentifier(dataTrigger.apikey, dataTrigger.namespace, dataTrigger.name);
@@ -115,7 +119,7 @@ module.exports = function(
                             if (retryCount < retryAttempts) {
                                 logger.info(method, 'attempting to fire trigger again', triggerIdentifier, 'Retry Count:', (retryCount + 1));
                                 setTimeout(function () {
-                                    utils.postTrigger(dataTrigger, payload, uri, auth, (retryCount + 1))
+                                    utils.postTrigger(dataTrigger, uri, auth, (retryCount + 1))
                                     .then(triggerId => {
                                         resolve(triggerId);
                                     })
@@ -154,8 +158,12 @@ module.exports = function(
         else if (dataTrigger.stopDate) {
             //check if the next scheduled trigger is after the stop date
             if (dataTrigger.cronHandle && dataTrigger.cronHandle.nextDate().isAfter(new Date(dataTrigger.stopDate))) {
-                utils.disableTrigger(triggerIdentifier, undefined, 'Automatically disabled after firing last scheduled trigger');
-                logger.info(method, 'last scheduled trigger before stop date, disabled', triggerIdentifier);
+                utils.disableTrigger(triggerIdentifier, undefined, 'Automatically disabled after firing last scheduled cron trigger');
+                logger.info(method, 'last scheduled cron trigger before stop date, disabled', triggerIdentifier);
+            }
+            else if (dataTrigger.minutes && (Date.now() + (dataTrigger.minutes * 1000 * 60) > new Date(dataTrigger.stopDate).getTime())) {
+                utils.disableTrigger(triggerIdentifier, undefined, 'Automatically disabled after firing last scheduled interval trigger');
+                logger.info(method, 'last scheduled interval trigger before stop date, disabled', triggerIdentifier);
             }
         }
         else if (dataTrigger.maxTriggers && dataTrigger.triggersLeft === 0) {
@@ -204,6 +212,9 @@ module.exports = function(
             if (utils.triggers[triggerIdentifier].cronHandle) {
                 utils.triggers[triggerIdentifier].cronHandle.stop();
             }
+            else if (utils.triggers[triggerIdentifier].intervalHandle) {
+                lt.clearInterval(utils.triggers[triggerIdentifier].intervalHandle);
+            }
             delete utils.triggers[triggerIdentifier];
             logger.info(method, 'trigger', triggerIdentifier, 'successfully deleted from memory');
         }
@@ -255,6 +266,13 @@ module.exports = function(
                                 .then(cachedTrigger => {
                                     utils.triggers[triggerIdentifier] = cachedTrigger;
                                     logger.info(method, triggerIdentifier, 'created successfully');
+                                    if (cachedTrigger.intervalHandle && utils.activeHost === utils.host) {
+                                        try {
+                                            utils.fireTrigger(cachedTrigger);
+                                        } catch (e) {
+                                            logger.error(method, 'Exception occurred while firing trigger', triggerIdentifier, e);
+                                        }
+                                    }
                                 })
                                 .catch(err => {
                                     var message = 'Automatically disabled after receiving error on trigger initialization: ' + err;
@@ -300,6 +318,13 @@ module.exports = function(
                         .then(cachedTrigger => {
                             utils.triggers[triggerIdentifier] = cachedTrigger;
                             logger.info(method, triggerIdentifier, 'created successfully');
+                            if (cachedTrigger.intervalHandle && utils.activeHost === utils.host) {
+                                try {
+                                    utils.fireTrigger(cachedTrigger);
+                                } catch (e) {
+                                    logger.error(method, 'Exception occurred while firing trigger', triggerIdentifier, e);
+                                }
+                            }
                         })
                         .catch(err => {
                             var message = 'Automatically disabled after receiving error on trigger creation: ' + err;
diff --git a/tests/src/test/scala/system/health/AlarmsHealthFeedTests.scala b/tests/src/test/scala/system/health/AlarmsHealthFeedTests.scala
index fb7c6d8..da901e8 100644
--- a/tests/src/test/scala/system/health/AlarmsHealthFeedTests.scala
+++ b/tests/src/test/scala/system/health/AlarmsHealthFeedTests.scala
@@ -22,7 +22,7 @@ import common.{TestHelpers, Wsk, WskProps, WskTestHelpers}
 import org.junit.runner.RunWith
 import org.scalatest.{FlatSpec, Inside}
 import org.scalatest.junit.JUnitRunner
-import spray.json.DefaultJsonProtocol.{LongJsonFormat, StringJsonFormat, BooleanJsonFormat}
+import spray.json.DefaultJsonProtocol.{IntJsonFormat, LongJsonFormat, StringJsonFormat, BooleanJsonFormat}
 import spray.json.{JsObject, JsString, pimpAny}
 
 /**
@@ -230,4 +230,45 @@ class AlarmsHealthFeedTests
             println("Activation list after wait should equal with activation list after stopDate")
             activationsAfterWait should be(activations)
     }
+
+    it should "fire interval trigger using startDate and stopDate" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            implicit val wskprops = wp // shadow global props and make implicit
+            val triggerName = s"dummyAlarmsTrigger-${System.currentTimeMillis}"
+            val packageName = "dummyAlarmsPackage"
+
+            // the package alarms should be there
+            val packageGetResult = wsk.pkg.get("/whisk.system/alarms")
+            println("fetched package alarms")
+            packageGetResult.stdout should include("ok")
+
+            // create package binding
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, name) => pkg.bind("/whisk.system/alarms", name)
+            }
+
+            val startDate = System.currentTimeMillis + (1000 * 20)
+            val stopDate = startDate + (1000 * 90)
+
+            println(s"Creating trigger: $triggerName")
+            // create whisk stuff
+            val feedCreationResult = assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) =>
+                    trigger.create(name, feed = Some(s"$packageName/interval"), parameters = Map(
+                        "minutes" -> 1.toJson,
+                        "startDate" -> startDate.toJson,
+                        "stopDate" -> stopDate.toJson))
+            }
+            feedCreationResult.stdout should include("ok")
+
+            println("waiting for start date")
+            val activations = wsk.activation.pollFor(N = 1, Some(triggerName), retries = 45).length
+            println(s"Found activation size (should be 1): $activations")
+            activations should be(1)
+
+            println("waiting for interval")
+            val activationsAfterInterval = wsk.activation.pollFor(N = 2, Some(triggerName), retries = 90).length
+            println(s"Found activation size (should be 2): $activationsAfterInterval")
+            activationsAfterInterval should be(2)
+    }
 }
diff --git a/tests/src/test/scala/system/packages/AlarmsFeedTests.scala b/tests/src/test/scala/system/packages/AlarmsFeedTests.scala
index 2ad09c7..876b070 100644
--- a/tests/src/test/scala/system/packages/AlarmsFeedTests.scala
+++ b/tests/src/test/scala/system/packages/AlarmsFeedTests.scala
@@ -290,4 +290,63 @@ class AlarmsFeedTests
             feedCreationResult.stderr should include(s"stopDate parameter '${stopDate}' must be greater than the startDate")
 
     }
+
+    it should "return error message when interval action does not include minutes parameter" in withAssetCleaner(wskprops) {
+
+        (wp, assetHelper) =>
+            implicit val wskprops = wp // shadow global props and make implicit
+            val triggerName = s"dummyCloudantTrigger-${System.currentTimeMillis}"
+            val packageName = "dummyCloudantPackage"
+            val feed = "interval"
+
+            // the package alarms should be there
+            val packageGetResult = wsk.pkg.get("/whisk.system/alarms")
+            println("fetched package alarms")
+            packageGetResult.stdout should include("ok")
+
+            // create package binding
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, name) => pkg.bind("/whisk.system/alarms", name)
+            }
+
+            // create whisk stuff
+            val feedCreationResult = assetHelper.withCleaner(wsk.trigger, triggerName, confirmDelete = false) {
+                (trigger, name) =>
+                    trigger.create(name, feed = Some(s"$packageName/$feed"), parameters = Map(
+                        "trigger_payload" -> "alarmTest".toJson),
+                        expectedExitCode = 246)
+            }
+            feedCreationResult.stderr should include("interval trigger feed is missing the minutes parameter")
+
+    }
+
+    it should "return error message when interval action includes invalid minutes parameter" in withAssetCleaner(wskprops) {
+
+        (wp, assetHelper) =>
+            implicit val wskprops = wp // shadow global props and make implicit
+            val triggerName = s"dummyCloudantTrigger-${System.currentTimeMillis}"
+            val packageName = "dummyCloudantPackage"
+            val feed = "interval"
+
+            // the package alarms should be there
+            val packageGetResult = wsk.pkg.get("/whisk.system/alarms")
+            println("fetched package alarms")
+            packageGetResult.stdout should include("ok")
+
+            // create package binding
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, name) => pkg.bind("/whisk.system/alarms", name)
+            }
+
+            // create whisk stuff
+            val feedCreationResult = assetHelper.withCleaner(wsk.trigger, triggerName, confirmDelete = false) {
+                (trigger, name) =>
+                    trigger.create(name, feed = Some(s"$packageName/$feed"), parameters = Map(
+                        "trigger_payload" -> "alarmTest".toJson,
+                        "minutes" -> "five".toJson),
+                        expectedExitCode = 246)
+            }
+            feedCreationResult.stderr should include("the minutes parameter must be an integer")
+
+    }
 }
diff --git a/tests/src/test/scala/system/packages/AlarmsFeedWebTests.scala b/tests/src/test/scala/system/packages/AlarmsFeedWebTests.scala
index a1f3060..cb8d41f 100644
--- a/tests/src/test/scala/system/packages/AlarmsFeedWebTests.scala
+++ b/tests/src/test/scala/system/packages/AlarmsFeedWebTests.scala
@@ -51,25 +51,25 @@ class AlarmsFeedWebTests
         wsk.action.get(webAction, FORBIDDEN)
     }
 
-    it should "reject put of a trigger due to missing triggerName argument" in {
+    it should "reject post of a trigger due to missing triggerName argument" in {
         val params = JsObject(originalParams.fields - "triggerName")
 
         makePostCallWithExpectedResult(params, JsObject("error" -> JsString("no trigger name parameter was provided")), 400)
     }
 
-    it should "reject put of a trigger due to missing cron argument" in {
+    it should "reject post of a trigger due to missing cron argument" in {
         val params = JsObject(originalParams.fields - "cron")
 
         makePostCallWithExpectedResult(params, JsObject("error" -> JsString("alarms trigger feed is missing the cron parameter")), 400)
     }
 
-    it should "reject put of a trigger due to invalid cron argument" in {
+    it should "reject post of a trigger due to invalid cron argument" in {
         val params = JsObject(originalParams.fields + ("cron" -> JsString("***")))
 
         makePostCallWithExpectedResult(params, JsObject("error" -> JsString("cron pattern '***' is not valid")), 400)
     }
 
-    it should "reject put of a trigger when authentication fails" in {
+    it should "reject post of a trigger when authentication fails" in {
         val params = JsObject(originalParams.fields + ("cron" -> JsString("* * * * *")))
         makePostCallWithExpectedResult(params, JsObject("error" -> JsString("Trigger authentication request failed.")), 401)
     }

-- 
To stop receiving notification emails like this one, please contact
['"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>'].