blob: f924db3688cde6e2f731cebe83f280fc516d4693 [file] [log] [blame]
/*
* Copyright 2015-2016 IBM Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package whisk.core.container.test
import scala.concurrent.Future
import scala.concurrent.duration._
import org.junit.runner.RunWith
import org.scalatest.BeforeAndAfter
import org.scalatest.BeforeAndAfterAll
import org.scalatest.FlatSpec
import org.scalatest.junit.JUnitRunner
import akka.event.Logging.ErrorLevel
import whisk.common.TransactionId
import whisk.core.WhiskConfig
import whisk.core.WhiskConfig.dockerEndpoint
import whisk.core.WhiskConfig.edgeHostName
import whisk.core.WhiskConfig.selfDockerEndpoint
import whisk.core.WhiskConfig.invokerSerializeDockerOp
import whisk.core.WhiskConfig.invokerSerializeDockerPull
import whisk.core.container.Container
import whisk.core.container.ContainerPool
import whisk.core.entity.AuthKey
import whisk.core.entity.EntityName
import whisk.core.entity.Exec
import whisk.core.entity.EntityPath
import whisk.core.entity.WhiskAction
import whisk.core.entity.WhiskAuthStore
import whisk.core.entity.WhiskEntityStore
import scala.language.postfixOps
import common.WskActorSystem
/**
* Unit tests for ContainerPool and, by association, Container and WhiskContainer.
*/
@RunWith(classOf[JUnitRunner])
class ContainerPoolTests extends FlatSpec
with BeforeAndAfter
with BeforeAndAfterAll
with WskActorSystem {
implicit val transid = TransactionId.testing
val config = new WhiskConfig(
WhiskEntityStore.requiredProperties ++
WhiskAuthStore.requiredProperties ++
ContainerPool.requiredProperties ++
Map(selfDockerEndpoint -> "localhost",
dockerEndpoint -> null,
edgeHostName -> "localhost",
invokerSerializeDockerOp -> "true",
invokerSerializeDockerPull -> "true"))
assert(config.isValid)
val pool = new ContainerPool(config, 0, ErrorLevel, true, true)
pool.logDir = "/tmp"
val datastore = WhiskEntityStore.datastore(config)
override def afterAll() {
println("Shutting down store connections")
datastore.shutdown()
super.afterAll()
}
/**
* Starts (and returns) a container running ubuntu image running echo on the given test word.
* Also checks that the test word shows up in the docker logs.
*/
def getEcho(word: String): Container = {
val conOpt = pool.getByImageName("ubuntu", Array("/bin/echo", word))
assert(conOpt isDefined) // we must be able to start the container
val con = conOpt.getOrElse(null)
Thread.sleep(1000) // docker run has no guarantee how far along the process is
assert(con.getLogs().contains(word)) // the word must be in the docker logs
con
}
/*
* Start a new container that stays around via sleep.
*/
def getSleep(duration: Int): Container = {
val conOpt = pool.getByImageName("ubuntu", Array("/bin/sleep", duration.toString()))
assert(conOpt isDefined) // we must be able to start the container
conOpt.getOrElse(null)
}
/*
* Ensure pool is empty/clean.
*/
def ensureClean() = {
pool.enableGC()
pool.forceGC()
Thread.sleep(2 * pool.gcFrequency.toMillis + 1500L) // GC should collect this by now
assert(pool.idleCount() == 0)
assert(pool.activeCount() == 0)
}
/*
* Does a container with the given prefix exist?
*/
def poolHasContainerIdPrefix(containerIdPrefix: String) = {
val states = pool.listAll()
states.find { _.id.hash.contains(containerIdPrefix) }.isDefined
}
behavior of "ContainerPool"
after {
ensureClean()
}
it should "be empty when it starts" in {
assert(pool.idleCount() == 0)
assert(pool.activeCount() == 0)
}
it should "allow getting container by image name, run it, retrieve logs, return it, force GC, check via docker ps" in {
pool.disableGC()
val startIdleCount = pool.idleCount()
val container = getEcho("abracadabra")
val containerIdPrefix = container.containerIdPrefix
assert(poolHasContainerIdPrefix(containerIdPrefix)) // container must be around
pool.putBack(container) // contractually, user must let go of con at this point
assert(pool.idleCount() == startIdleCount + 1)
pool.enableGC()
pool.forceGC() // force all containers in pool to be freed
Thread.sleep(2 * pool.gcFrequency.toMillis + 1500L) // GC should collect this by now
assert(!poolHasContainerIdPrefix(containerIdPrefix)) // container must be gone by now
assert(pool.idleCount() == 0)
}
it should "respect maxIdle by shooting a container on a putBack that could exceed it" in {
ensureClean()
pool.maxIdle = 1
val c1 = getEcho("quasar")
val c2 = getEcho("pulsar")
val p1 = c1.containerIdPrefix
val p2 = c2.containerIdPrefix
assert(pool.activeCount() == 2)
assert(pool.idleCount() == 0)
pool.putBack(c1)
assert(pool.activeCount() == 1)
assert(pool.idleCount() == 1)
pool.putBack(c2)
assert(pool.activeCount() == 0)
assert(pool.idleCount() == 1) // because c1 got shot
pool.resetMaxIdle()
}
it should "respect activeIdle by blocking a getContainer until another is returned" in {
ensureClean()
pool.maxActive = 1
val c1 = getEcho("hocus")
var c1Back = false
val f = Future { Thread.sleep(3000); c1Back = true; pool.putBack(c1) }
val c2 = getEcho("pocus")
assert(c1Back) // make sure c2 is not available before c1 is put back
pool.putBack(c2)
pool.resetMaxActive()
}
it should "also perform automatic GC with a settable threshold, invoke same action afterwards, another GC" in {
ensureClean();
pool.gcThreshold = 1.seconds
val container = getEcho("hocus pocus")
val containerIdPrefix = container.containerIdPrefix
assert(poolHasContainerIdPrefix(containerIdPrefix)) // container must be around
pool.putBack(container); // contractually, user must let go of con at this point
// TODO: replace this with GC count so we don't break abstraction by knowing the GC check freq. (!= threshold)
Thread.sleep(2 * pool.gcFrequency.toMillis + 4000L) // GC should collect this by now
assert(!poolHasContainerIdPrefix(containerIdPrefix)) // container must be gone by now
// Do it again now
val container2 = getEcho("hocus pocus")
val containerIdPrefix2 = container2.containerIdPrefix
assert(poolHasContainerIdPrefix(containerIdPrefix2)) // container must be around
pool.putBack(container2)
pool.resetGCThreshold()
}
// Lower it some more by parameterizing GC thresholds
it should "be able to go through 15 containers without thrashing the system" in {
ensureClean()
val max = 15
for (i <- List.range(0, max)) {
val name = "foobar" + i
val action = makeHelloAction(name, i)
pool.getAction(action, defaultAuth) match {
case (con, initResult) => {
val str = "QWERTY" + i.toString()
con.run(str, (20000 + i).toString()) // payload + activationId
if (i == max - 1) {
Thread.sleep(1000)
assert(con.getLogs().contains(str))
}
pool.putBack(con)
}
} // match
} // for
}
private val defaultNamespace = EntityPath("container pool test")
private val defaultAuth = AuthKey()
/*
* Create an action with the given name that print hello_N payload !
* where N is specified.
*/
private def makeHelloAction(name: String, index: Integer): WhiskAction = {
val code = """console.log('ABCXYZ'); function main(msg) { console.log('hello_${index}', msg.payload+'!');} """
WhiskAction(defaultNamespace, EntityName(name), Exec.js(code))
}
it should "be able to start a nodejs action with init, do a run, return to pool, do another get testing reuse, another run" in {
ensureClean()
val action = makeHelloAction("foobar", 0)
// Make a whisk container and test init and a push
val (con, initRes) = pool.getAction(action, defaultAuth)
Thread.sleep(1000)
assert(con.getLogs().contains("ABCXYZ"))
con.run("QWERTY", "55555") // payload + activationId
Thread.sleep(1000)
assert(con.getLogs().contains("QWERTY"))
pool.putBack(con)
// Test container reuse
val (con2, _) = pool.getAction(action, defaultAuth)
assert(con == con2) // check re-use
con.run("ASDFGH", "4444") // payload + activationId
Thread.sleep(1000)
assert(con.getLogs().contains("ASDFGH"))
pool.putBack(con)
}
}