All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.allegro.tech.servicemesh.envoycontrol.routing.CanaryLoadBalancingTest.kt Maven / Gradle / Ivy

There is a newer version: 0.22.1
Show newest version
package pl.allegro.tech.servicemesh.envoycontrol.routing

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import pl.allegro.tech.servicemesh.envoycontrol.assertions.isOk
import pl.allegro.tech.servicemesh.envoycontrol.assertions.untilAsserted
import pl.allegro.tech.servicemesh.envoycontrol.config.consul.ConsulExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.CallStats
import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.EnvoyExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.envoy.ResponseWithBody
import pl.allegro.tech.servicemesh.envoycontrol.config.envoycontrol.EnvoyControlExtension
import pl.allegro.tech.servicemesh.envoycontrol.config.service.EchoServiceExtension

open class CanaryLoadBalancingTest {

    companion object {
        private val properties = mapOf(
            "envoy-control.source.consul.tags.weight" to "weight",
            "envoy-control.source.consul.tags.canary" to "canary",
            "envoy-control.envoy.snapshot.load-balancing.weights.enabled" to true,
            "envoy-control.envoy.snapshot.load-balancing.canary.enabled" to true,
            "envoy-control.envoy.snapshot.load-balancing.canary.metadata-key" to "canary",
            "envoy-control.envoy.snapshot.load-balancing.canary.metadata-value" to "1"
        )

        @JvmField
        @RegisterExtension
        val consul = ConsulExtension()

        @JvmField
        @RegisterExtension
        val envoyControl = EnvoyControlExtension(consul, properties)

        @JvmField
        @RegisterExtension
        val canaryContainer = EchoServiceExtension()

        @JvmField
        @RegisterExtension
        val regularContainer = EchoServiceExtension()

        @JvmField
        @RegisterExtension
        val envoy = EnvoyExtension(envoyControl)
    }

    @Test
    fun `should balance load according to weights`() {
        // given
        consul.server.operations.registerService(name = "echo", extension = canaryContainer(), tags = listOf("canary", "weight:1"))
        consul.server.operations.registerService(name = "echo", extension = regularContainer(), tags = listOf("weight:20"))

        untilAsserted {
            envoy().egressOperations.callService("echo").also {
                assertThat(it).isOk()
            }
        }

        // when
        val stats = callEchoServiceRepeatedly(
            minRepeat = 30,
            maxRepeat = 200,
            repeatUntil = { response -> response.isFrom(canaryContainer().container()) }
        )

        // then
        assertThat(stats.canaryHits + stats.regularHits).isEqualTo(stats.totalHits)
        assertThat(stats.totalHits).isGreaterThan(29)
        assertThat(stats.canaryHits).isGreaterThan(0)
        /**
         * The condition below tests if the regular instance received at least 3x more requests than the canary
         * instance.
         * According to weights, the regular instance should receive approximately 20x more requests than the canary
         * instance. But load balancing is a random process, so we cannot assume that 20x factor will be achieved every
         * time.
         *
         * The test is not 100% deterministic. There is very little chance, that the test will
         * fail even if everything is ok (false negative) or will pass even if something is wrong (false positive).
         * The factor of 3 is chosen to minimize the chance of both false result types:
         *
         * False positive -> if weighted load balancing doesn't work correctly, instances should receive approximately
         * the same number of requests. There will be at least 30 request. The chance that regular instance will
         * receive more than 3/4 of them is very small.
         *
         * False negative -> if weighted load balancing works correctly, with at least 30 requests, the chance that
         * the regular instance will receive less than 3/4 of them is very small.
         */
        assertThat(stats.regularHits).isGreaterThan(stats.canaryHits * 3)
    }

    @Test
    fun `should route request to canary instance only`() {
        // given
        consul.server.operations.registerService(name = "echo", extension = canaryContainer(), tags = listOf("canary", "weight:1"))
        consul.server.operations.registerService(name = "echo", extension = regularContainer(), tags = listOf("weight:20"))

        untilAsserted {
            envoy().egressOperations.callService("echo").also {
                assertThat(it).isOk()
            }
        }

        // when
        val stats = callEchoServiceRepeatedly(
            minRepeat = 50,
            maxRepeat = 50,
            headers = mapOf("X-Canary" to "1")
        )

        // then
        assertThat(stats.totalHits).isEqualTo(50)
        assertThat(stats.canaryHits).isEqualTo(50)
        assertThat(stats.regularHits).isEqualTo(0)
    }

    @Test
    open fun `should route to both canary and regular instances when canary weight is 0`() {
        consul.server.operations.registerService(name = "echo", extension = canaryContainer(), tags = listOf("canary", "weight:0"))
        consul.server.operations.registerService(name = "echo", extension = regularContainer(), tags = listOf("weight:20"))

        untilAsserted {
            envoy().egressOperations.callService("echo").also {
                assertThat(it).isOk()
            }
        }

        // when
        val stats = callEchoServiceRepeatedly(
            minRepeat = 30,
            maxRepeat = 200,
            repeatUntil = { response -> response.isFrom(canaryContainer().container()) }
        )

        // then
        assertThat(stats.canaryHits + stats.regularHits).isEqualTo(stats.totalHits)
        assertThat(stats.totalHits).isGreaterThan(29)
        assertThat(stats.canaryHits).isGreaterThan(0)
    }

    protected open fun callStats() = CallStats(listOf(canaryContainer(), regularContainer()))

    fun callEchoServiceRepeatedly(
        minRepeat: Int,
        maxRepeat: Int,
        repeatUntil: (ResponseWithBody) -> Boolean = { false },
        headers: Map = mapOf()
    ): CallStats {
        val stats = callStats()
        envoy().egressOperations.callServiceRepeatedly(
            service = "echo",
            stats = stats,
            minRepeat = minRepeat,
            maxRepeat = maxRepeat,
            repeatUntil = repeatUntil,
            headers = headers
        )
        return stats
    }

    val CallStats.regularHits: Int
        get() = this.hits(regularContainer())
    val CallStats.canaryHits: Int
        get() = this.hits(canaryContainer())

    open fun envoyControl() = envoyControl

    open fun envoy() = envoy

    open fun consul() = consul

    open fun canaryContainer() = canaryContainer

    open fun regularContainer() = regularContainer
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy