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

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


The following commit(s) were added to refs/heads/master by this push:
     new c58ed88  Add parallel, map, and dynamic combinators (#16)
c58ed88 is described below

commit c58ed88f368aa0a97b06838ac3063bf7a98f2257
Author: Olivier Tardieu <[email protected]>
AuthorDate: Sat Jan 19 17:06:50 2019 -0500

    Add parallel, map, and dynamic combinators (#16)
    
    * Add parallel, map, and dynamic combinators
---
 .travis.yml          |   1 +
 README.md            |  55 ++++++++++++++++---
 composer.js          |   8 ++-
 conductor.js         | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 docs/COMBINATORS.md  |  70 ++++++++++++++++++++++++
 docs/COMPOSITIONS.md |   8 +--
 test/composer.js     |  16 ++++++
 test/conductor.js    |  45 ++++++++++++++++
 travis/setup.sh      |   3 ++
 9 files changed, 344 insertions(+), 12 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 5ec856f..051e253 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,6 +23,7 @@ services:
 env:
   global:
     - IGNORE_CERTS=true
+    - REDIS=redis://172.17.0.1:6379
 before_install:
   - ./travis/scancode.sh
 before_script:
diff --git a/README.md b/README.md
index c3554aa..9fa0af1 100644
--- a/README.md
+++ b/README.md
@@ -65,12 +65,12 @@ module.exports = composer.if(
     composer.action('failure', { action: function () { return { message: 
'failure' } } }))
 ```
 Compositions compose actions using [combinator](docs/COMBINATORS.md) methods.
-These methods implement the typical control-flow constructs of a sequential
-imperative programming language. This example composition composes three 
actions
-named `authenticate`, `success`, and `failure` using the `composer.if`
-combinator, which implements the usual conditional construct. It take three
-actions (or compositions) as parameters. It invokes the first one and, 
depending
-on the result of this invocation, invokes either the second or third action.
+These methods implement the typical control-flow constructs of an imperative
+programming language. This example composition composes three actions named
+`authenticate`, `success`, and `failure` using the `composer.if` combinator,
+which implements the usual conditional construct. It take three actions (or
+compositions) as parameters. It invokes the first one and, depending on the
+result of this invocation, invokes either the second or third action.
 
  This composition includes the definitions of the three composed actions. If 
the
  actions are defined and deployed elsewhere, the composition code can be 
shorten
@@ -143,6 +143,49 @@ Compositions are implemented by means of OpenWhisk 
conductor actions. The
 
actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md)
 explains execution traces in greater details.
 
+While composer does not limit in principle the length of a composition,
+OpenWhisk deployments typically enforce a limit on the number of action
+invocations in a composition as well as an upper bound on the rate of
+invocation. These limits may result in compositions failing to execute to
+completion.
+
+## Parallel compositions with Redis
+
+Composer offers parallel combinators that make it possible to run actions or
+compositions in parallel, for example:
+```javascript
+composer.parallel('checkInventory', 'detectFraud')
+```
+
+The width of parallel compositions is not in principle limited by composer, but
+issuing many concurrent invocations may hit OpenWhisk limits leading to
+failures: failure to execute a branch of a parallel composition or failure to
+complete the parallel composition.
+
+These combinators require access to a Redis instance to hold intermediate
+results of parallel compositions. The Redis credentials may be specified at
+invocation time or earlier by means of default parameters or package bindings.
+The required parameter is named `$composer`. It is a dictionary with a `redis`
+field of type dictionary. The `redis` dictionary specifies the `uri` for the
+Redis instance and optionally a certificate as a base64-encoded string to 
enable
+tls connections. Hence, the input parameter object for our order-processing
+example should be:
+```json
+{
+    "$composer": {
+        "redis": {
+            "uri": "redis://...",
+            "ca": "optional base64 encoded tls certificate"
+        }
+    },
+    "order": { ... }
+}
+```
+
+The intent is to store intermediate results in Redis as the parallel 
composition
+is progressing. Redis entries are deleted after completion and, as an added
+safety, expire after twenty-four hours.
+
 # Disclaimer
 
 Apache OpenWhisk Composer is an effort undergoing incubation at The Apache 
Software Foundation (ASF), sponsored by the Apache Incubator. Incubation is 
required of all newly accepted projects until a further review indicates that 
the infrastructure, communications, and decision making process have stabilized 
in a manner consistent with other successful ASF projects. While incubation 
status is not necessarily a reflection of the completeness or stability of the 
code, it does indicate that  [...]
diff --git a/composer.js b/composer.js
index ed3ef97..66f500f 100644
--- a/composer.js
+++ b/composer.js
@@ -285,7 +285,10 @@ const combinators = {
   mask: { components: true },
   action: { args: [{ name: 'name', type: 'name' }, { name: 'action', type: 
'object', optional: true }] },
   function: { args: [{ name: 'function', type: 'object' }] },
-  async: { components: true }
+  async: { components: true },
+  parallel: { components: true },
+  map: { components: true },
+  dynamic: {}
 }
 
 Object.assign(composer, declare(combinators))
@@ -303,7 +306,8 @@ const extra = {
   retain_catch: { components: true, def: lowerer.retain_catch },
   value: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal },
   literal: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal },
-  merge: { components: true, def: lowerer.merge }
+  merge: { components: true, def: lowerer.merge },
+  par: { components: true, def: composer.parallel }
 }
 
 Object.assign(composer, declare(extra))
diff --git a/conductor.js b/conductor.js
index aca9fb1..ac595a4 100644
--- a/conductor.js
+++ b/conductor.js
@@ -83,10 +83,79 @@ class Compositions {
 // runtime code
 function main (composition) {
   const openwhisk = require('openwhisk')
+  const redis = require('redis')
+  const uuid = require('uuid').v4
   let wsk
+  let db
+  const expiration = 86400 // expire redis key after a day
+
+  function live (id) { return `composer/fork/${id}` }
+  function done (id) { return `composer/join/${id}` }
+
+  function createRedisClient (p) {
+    const client = redis.createClient(p.s.redis.uri, p.s.redis.ca ? { tls: { 
ca: Buffer.from(p.s.redis.ca, 'base64').toString('binary') } } : {})
+    const noop = () => { }
+    let handler = noop
+    client.on('error', error => handler(error))
+    require('redis-commands').list.forEach(f => {
+      client[`${f}Async`] = function () {
+        let failed = false
+        return new Promise((resolve, reject) => {
+          handler = error => {
+            handler = noop
+            failed = true
+            reject(error)
+          }
+          client[f](...arguments, (error, result) => {
+            handler = noop
+            return error ? reject(error) : resolve(result)
+          })
+        }).catch(error => {
+          if (failed) client.end(true)
+          return Promise.reject(error)
+        })
+      }
+    })
+    return client
+  }
 
   const isObject = obj => typeof obj === 'object' && obj !== null && 
!Array.isArray(obj)
 
+  function fork ({ p, node, index }, array, it) {
+    const saved = p.params // save params
+    p.s.state = index + node.return // return state
+    p.params = { value: [] } // return value
+    if (array.length === 0) return
+    if (typeof p.s.redis !== 'object' || typeof p.s.redis.uri !== 'string' || 
(typeof p.s.redis.ca !== 'string' && typeof p.s.redis.ca !== 'undefined')) {
+      p.params = { error: 'Parallel combinator requires a properly configured 
redis instance' }
+      console.error(p.params.error)
+      return
+    }
+    const stack = [{ marker: true }].concat(p.s.stack)
+    const barrierId = uuid()
+    console.log(`barrierId: ${barrierId}, spawning: ${array.length}`)
+    if (!wsk) wsk = openwhisk({ ignore_certs: true })
+    if (!db) db = createRedisClient(p)
+    return db.lpushAsync(live(barrierId), 42) // push marker
+      .then(() => db.expireAsync(live(barrierId), expiration))
+      .then(() => Promise.all(array.map((item, position) => {
+        const params = it(saved, item) // obtain combinator-specific params 
for branch invocation
+        params.$composer.stack = stack
+        params.$composer.redis = p.s.redis
+        params.$composer.join = { barrierId, position, count: array.length }
+        return wsk.actions.invoke({ name: process.env.__OW_ACTION_NAME, params 
}) // invoke branch
+          .then(({ activationId }) => { console.log(`barrierId: ${barrierId}, 
spawned position: ${position} with activationId: ${activationId}`) })
+      }))).then(() => collect(p, barrierId), error => {
+        console.error(error.body || error)
+        p.params = { error: `Parallel combinator failed to invoke a 
composition at AST node root${node.parent} (see log for details)` }
+        return db.delAsync(live(barrierId), done(barrierId)) // delete keys
+          .then(() => {
+            inspect(p)
+            return step(p)
+          })
+      })
+  }
+
   // compile ast to fsm
   const compiler = {
     sequence (parent, node) {
@@ -148,6 +217,23 @@ function main (composition) {
       const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.body), 
...compile(parent, node.test), { parent, type: 'choice', else: 1 }, { parent, 
type: 'pass' }]
       fsm[fsm.length - 2].then = 2 - fsm.length
       return fsm
+    },
+
+    parallel (parent, node) {
+      const tasks = node.components.map(task => [...compile(parent, task), { 
parent, type: 'stop' }])
+      const fsm = [{ parent, type: 'parallel' }, ...tasks.reduce((acc, cur) => 
{ acc.push(...cur); return acc }, []), { parent, type: 'pass' }]
+      fsm[0].return = fsm.length - 1
+      fsm[0].tasks = tasks.reduce((acc, cur) => { acc.push(acc[acc.length - 1] 
+ cur.length); return acc }, [1]).slice(0, -1)
+      return fsm
+    },
+
+    map (parent, node) {
+      const tasks = compile(parent, ...node.components)
+      return [{ parent, type: 'map', return: tasks.length + 2 }, ...tasks, { 
parent, type: 'stop' }, { parent, type: 'pass' }]
+    },
+
+    dynamic (parent, node) {
+      return [{ parent, type: 'dynamic' }]
     }
   }
 
@@ -208,7 +294,7 @@ function main (composition) {
     },
 
     async ({ p, node, index, inspect, step }) {
-      p.params.$composer = { state: p.s.state, stack: [{ marker: true 
}].concat(p.s.stack) }
+      p.params.$composer = { state: p.s.state, stack: [{ marker: true 
}].concat(p.s.stack), redis: p.s.redis }
       p.s.state = index + node.return
       if (!wsk) wsk = openwhisk({ ignore_certs: true })
       return wsk.actions.invoke({ name: process.env.__OW_ACTION_NAME, params: 
p.params })
@@ -225,6 +311,31 @@ function main (composition) {
 
     stop ({ p, node, index, inspect, step }) {
       p.s.state = -1
+    },
+
+    parallel ({ p, node, index }) {
+      return fork({ p, node, index }, node.tasks, (input, branch) => {
+        const params = Object.assign({}, input) // clone
+        params.$composer = { state: index + branch }
+        return params
+      })
+    },
+
+    map ({ p, node, index }) {
+      return fork({ p, node, index }, p.params.value || [], (input, branch) => 
{
+        const params = isObject(branch) ? branch : { value: branch } // wrap
+        params.$composer = { state: index + 1 }
+        return params
+      })
+    },
+
+    dynamic ({ p, node, index }) {
+      if (p.params.type !== 'action' || typeof p.params.name !== 'string' || 
typeof p.params.params !== 'object') {
+        p.params = { error: `Incorrect use of the dynamic combinator at AST 
node root${node.parent}` }
+        inspect(p)
+      } else {
+        return { method: 'action', action: p.params.name, params: 
p.params.params, state: { $composer: p.s } }
+      }
     }
   }
 
@@ -232,6 +343,29 @@ function main (composition) {
     return p.params.error ? p.params : { params: p.params }
   }
 
+  function collect (p, barrierId) {
+    if (!db) db = createRedisClient(p)
+    const timeout = Math.max(Math.floor((process.env.__OW_DEADLINE - new 
Date()) / 1000) - 5, 1)
+    console.log(`barrierId: ${barrierId}, waiting with timeout: ${timeout}s`)
+    return db.brpopAsync(done(barrierId), timeout) // pop marker
+      .then(marker => {
+        console.log(`barrierId: ${barrierId}, done waiting`)
+        if (marker !== null) {
+          return db.lrangeAsync(done(barrierId), 0, -1)
+            .then(result => result.map(JSON.parse).map(({ position, params }) 
=> { p.params.value[position] = params }))
+            .then(() => db.delAsync(live(barrierId), done(barrierId))) // 
delete keys
+            .then(() => {
+              inspect(p)
+              return step(p)
+            })
+        } else { // timeout
+          p.s.collect = barrierId
+          console.log(`barrierId: ${barrierId}, handling timeout`)
+          return { method: 'action', action: '/whisk.system/utils/echo', 
params: p.params, state: { $composer: p.s } }
+        }
+      })
+  }
+
   const internalError = error => Promise.reject(error) // terminate 
composition execution and record error
 
   // wrap params if not a dictionary, branch to error handler if error
@@ -288,6 +422,14 @@ function main (composition) {
     if (p.s.state < 0 || p.s.state >= fsm.length) {
       console.log(`Entering final state`)
       console.log(JSON.stringify(p.params))
+      if (p.s.join) {
+        if (!db) db = createRedisClient(p)
+        return db.lpushxAsync(live(p.s.join.barrierId), JSON.stringify({ 
position: p.s.join.position, params: p.params })).then(count => { // push only 
if marker is present
+          return (count > p.s.join.count ? 
db.renameAsync(live(p.s.join.barrierId), done(p.s.join.barrierId)) : 
Promise.resolve())
+        }).then(() => {
+          p.params = { method: 'join', sessionId: p.s.session, barrierId: 
p.s.join.barrierId, position: p.s.join.position }
+        })
+      }
       return
     }
 
@@ -315,6 +457,12 @@ function main (composition) {
       if (typeof p.s.state !== 'number') return internalError('state parameter 
is not a number')
       if (!Array.isArray(p.s.stack)) return internalError('stack parameter is 
not an array')
 
+      if (p.s.collect) { // waiting on parallel branches
+        const barrierId = p.s.collect
+        delete p.s.collect
+        return collect(p, barrierId)
+      }
+
       if ($composer.resuming) inspect(p) // handle error objects when resuming
 
       return step(p)
diff --git a/docs/COMBINATORS.md b/docs/COMBINATORS.md
index 28540e9..d3d9cd2 100644
--- a/docs/COMBINATORS.md
+++ b/docs/COMBINATORS.md
@@ -26,14 +26,17 @@ The `composer` module offers a number of combinators to 
define compositions:
 | [`action`](#action) | named action | `composer.action('echo')` |
 | [`async`](#async) | asynchronous invocation | `composer.async('compress', 
'upload')` |
 | [`dowhile` and `dowhile_nosave`](#dowhile) | loop at least once | 
`composer.dowhile('fetchData', 'needMoreData')` |
+| [`dynamic`](#dynamic) | dynamic invocation | `composer.dynamic()`
 | [`empty`](#empty) | empty sequence | `composer.empty()`
 | [`finally`](#finally) | finalization | `composer.finally('tryThis', 
'doThatAlways')` |
 | [`function`](#function) | Javascript function | `composer.function(({ x, y 
}) => ({ product: x * y }))` |
 | [`if` and `if_nosave`](#if) | conditional | `composer.if('authenticate', 
'success', 'failure')` |
 | [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 
'hello' }, ...)` |
 | [`literal` or `value`](#literal) | constant value | `composer.literal({ 
message: 'Hello, World!' })` |
+| [`map`](#map) | parallel map | `composer.map('validate', 'compute')` |
 | [`mask`](#mask) | variable hiding | `composer.let({ n }, composer.while(_ => 
n-- > 0, composer.mask(composition)))` |
 | [`merge`](#merge) | data augmentation | `composer.merge('hash')` |
+| [`parallel` or `par`](#parallel) | parallel composition | 
`composer.parallel('compress', 'hash')` |
 | [`repeat`](#repeat) | counted loop | `composer.repeat(3, 'hello')` |
 | [`retain` and `retain_catch`](#retain) | persistence | 
`composer.retain('validateInput')` |
 | [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` |
@@ -425,3 +428,70 @@ composer.seq(composer.retain(composition_1, composition_2, 
...), ({ params, resu
 compositions asynchronously. It invokes the sequence but does not wait for it 
to
 execute. It immediately returns a dictionary that includes a field named
 `activationId` with the activation id for the sequence invocation.
+
+The spawned sequence operates on a copy of the execution context for the parent
+composition. Variables declared in the parent are defined for the child and are
+initialized with the parent values at the time of the `async`. But mutations or
+later declarations in the parent are not visible in the child and vice versa.
+
+## Parallel
+
+Parallel combinators require access to a Redis instance as discussed
+[here](../README.md#parallel-compositions-with-redis).
+
+`composer.parallel(composition_1, composition_2, ...)` and its synonymous
+`composer.par(composition_1, composition_2, ...)` invoke a series of
+compositions (possibly empty) in parallel.
+
+This combinator runs _composition_1_, _composition_2_, ... in parallel and 
waits
+for all of these compositions to complete.
+
+The input parameter object for the composition is the input parameter object 
for
+every branch in the composition. The output parameter object for the 
composition
+has a single field named `value` of type array. The elements of the array are
+the output parameter objects for the branches in order.
+
+The `composer.let` variables in scope at the `parallel` combinator are in scope
+in the branches. But each branch has its own copy of the execution context.
+Variable mutations in one branch are not reflected in other branches or in the
+parent composition.
+
+## Map
+
+Parallel combinators require access to a Redis instance as discussed
+[here](../README.md#parallel-compositions-with-redis).
+
+`composer.map(composition_1, composition_2, ...)` makes multiple parallel
+invocations of a sequence of compositions.
+
+The input parameter object for the `map` combinator should include an array of
+named _value_. The `map` combinator spawns one sequence for each element of 
this
+array. The input parameter object for the nth instance of the sequence is the
+nth array element if it is a dictionary or an object with a single field named
+`value` with the nth array element as the field value. Fields on the input
+parameter object other than the `value` field are discarded. These sequences 
run
+in parallel. The `map` combinator waits for all the sequences to complete. The
+output parameter object for the composition has a single field named `value` of
+type array. The elements of the array are the output parameter objects for the
+branches in order.
+
+The `composer.let` variables in scope at the `map` combinator are in scope in
+the branches. But each branch has its own copy of the execution context.
+Variable mutations in one branch are not reflected in other branches or in the
+parent composition.
+
+## Dynamic
+
+`composer.dynamic()` invokes an action specified by means of the input 
parameter
+object.
+
+The input parameter object for the `dynamic` combinator must be a dictionary
+including the following three fields:
+- a field `type` with string value `"action"`,
+- a field `name` of type string,
+- a field `params` of type dictionary.
+Other fields of the input parameter object are ignored.
+
+The `dynamic` combinator invokes the action named _name_ with the input
+parameter object _params_. The output parameter object for the composition is
+the output parameter object of the action invocation.
diff --git a/docs/COMPOSITIONS.md b/docs/COMPOSITIONS.md
index 045d248..342b4f6 100644
--- a/docs/COMPOSITIONS.md
+++ b/docs/COMPOSITIONS.md
@@ -25,13 +25,15 @@ _compositions_. An example composition is described in
 
 ## Control flow
 
-Compositions can express the control flow of typical a sequential imperative
-programming language: sequences, conditionals, loops, structured error 
handling.
-This control flow is specified using _combinator_ methods such as:
+Compositions can express the control flow of typical imperative programming
+language: sequences, conditionals, loops, structured error handling. This
+control flow is specified using _combinator_ methods such as:
 - `composer.sequence(firstAction, secondAction)`
 - `composer.if(conditionAction, consequentAction, alternateAction)`
 - `composer.try(bodyAction, handlerAction)`
 
+Parallel constructs are also available.
+
 Combinators are described in [COMBINATORS.md](COMBINATORS.md).
 
 ## Composition objects
diff --git a/test/composer.js b/test/composer.js
index eca428d..ce0fc0c 100644
--- a/test/composer.js
+++ b/test/composer.js
@@ -414,4 +414,20 @@ describe('composer', function () {
   describe('composer.merge', function () {
     check('merge')
   })
+
+  describe('composer.parallel', function () {
+    check('parallel')
+  })
+
+  describe('composer.par', function () {
+    check('par')
+  })
+
+  describe('composer.map', function () {
+    check('map')
+  })
+
+  describe('composer.dynamic', function () {
+    check('dynamic', 0)
+  })
 })
diff --git a/test/conductor.js b/test/conductor.js
index da3324d..90b936e 100644
--- a/test/conductor.js
+++ b/test/conductor.js
@@ -33,12 +33,17 @@ const invoke = (composition, params = {}, blocking = true) 
=> wsk.compositions.d
   .then(() => wsk.actions.invoke({ name, params, blocking }))
   .then(activation => activation.response.success ? activation : 
Promise.reject(Object.assign(new Error(), { error: activation })))
 
+// redis configuration
+const redis = process.env.REDIS ? { uri: process.env.REDIS } : false
+if (process.env.REDIS && process.env.REDIS_CA) redis.ca = process.env.REDIS_CA
+
 describe('composer', function () {
   let n, x, y // dummy variables
 
   this.timeout(60000)
 
   before('deploy test actions', function () {
+    if (!redis) 
console.error('------------------------------------------------\nMissing redis 
configuration, skipping some 
tests\n------------------------------------------------')
     return define({ name: 'echo', action: 'const main = x=>x' })
       .then(() => define({ name: 'DivideByTwo', action: 'function main({n}) { 
return { n: n / 2 } }' }))
       .then(() => define({ name: 'TripleAndIncrement', action: 'function 
main({n}) { return { n: n * 3 + 1 } }' }))
@@ -114,6 +119,28 @@ describe('composer', function () {
       })
     })
 
+    describe('dynamic', function () {
+      it('dynamic action invocation', function () {
+        return invoke(composer.dynamic(), { type: 'action', name: 
'DivideByTwo', params: { n: 42 } }).then(activation => 
assert.deepStrictEqual(activation.response.result, { n: 21 }))
+      })
+
+      it('missing type', function () {
+        return invoke(composer.dynamic(), { name: 'DivideByTwo', params: { n: 
42 } }).then(() => assert.fail(), activation => 
assert.ok(activation.error.response.result.error))
+      })
+
+      it('invalid type', function () {
+        return invoke(composer.dynamic(), { type: 42, name: 'DivideByTwo', 
params: { n: 42 } }).then(() => assert.fail(), activation => 
assert.ok(activation.error.response.result.error))
+      })
+
+      it('missing name', function () {
+        return invoke(composer.dynamic(), { type: 'action', params: { n: 42 } 
}).then(() => assert.fail(), activation => 
assert.ok(activation.error.response.result.error))
+      })
+
+      it('missing params', function () {
+        return invoke(composer.dynamic(), { type: 'action', name: 
'DivideByTwo' }).then(() => assert.fail(), activation => 
assert.ok(activation.error.response.result.error))
+      })
+    })
+
     describe('literals', function () {
       it('true', function () {
         return invoke(composer.literal(true)).then(activation => 
assert.deepStrictEqual(activation.response.result, { value: true }))
@@ -291,6 +318,24 @@ describe('composer', function () {
         })
       })
 
+      describe('parallel', function () {
+        const test = redis ? it : it.skip
+        test('parallel', function () {
+          return invoke(composer.parallel('TripleAndIncrement', 
'DivideByTwo'), { n: 42, $composer: { redis } })
+            .then(activation => 
assert.deepStrictEqual(activation.response.result, { value: [{ n: 127 }, { n: 
21 }] }))
+        })
+
+        test('par', function () {
+          return invoke(composer.par('DivideByTwo', 'TripleAndIncrement', 
'isEven'), { n: 42, $composer: { redis } })
+            .then(activation => 
assert.deepStrictEqual(activation.response.result, { value: [{ n: 21 }, { n: 
127 }, { value: true }] }))
+        })
+
+        test('map', function () {
+          return invoke(composer.map('TripleAndIncrement', 'DivideByTwo'), { 
value: [{ n: 3 }, { n: 5 }, { n: 7 }], $composer: { redis } })
+            .then(activation => 
assert.deepStrictEqual(activation.response.result, { value: [{ n: 5 }, { n: 8 
}, { n: 11 }] }))
+        })
+      })
+
       describe('if', function () {
         it('condition = true', function () {
           return invoke(composer.if('isEven', 'DivideByTwo', 
'TripleAndIncrement'), { n: 4 })
diff --git a/travis/setup.sh b/travis/setup.sh
index e7bdd1b..82ebf3f 100755
--- a/travis/setup.sh
+++ b/travis/setup.sh
@@ -50,6 +50,9 @@ $ANSIBLE_CMD initdb.yml
 $ANSIBLE_CMD wipe.yml
 $ANSIBLE_CMD openwhisk.yml -e cli_installation_mode=remote -e 
limit_invocations_per_minute=600
 
+# Deploy Redis
+docker run -d -p 6379:6379 --name redis redis:4.0
+
 # Log configuration
 docker images
 docker ps

Reply via email to