-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(eureka): Handles duplicate eureka records (#3735)
This adds deterministic handling when we get multiple records for the same instance id. Previously we would potentially generate two cache data objects, however since they both had the same id only one would be written (last wins style most likely). This now prefers a record in a 'worse' state (based on ordering the HealthState enum from worst to best) to ensure that an instance showing as Down is not accidentally considered ready for traffic during a deploy. If all records agree on health state then takes the most recently updated.
- Loading branch information
Showing
4 changed files
with
176 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
...ovy/com/netflix/spinnaker/clouddriver/eureka/provider/agent/EurekaCachingAgentSpec.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package com.netflix.spinnaker.clouddriver.eureka.provider.agent | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.netflix.spinnaker.cats.provider.ProviderCache | ||
import com.netflix.spinnaker.clouddriver.eureka.api.EurekaApi | ||
import com.netflix.spinnaker.clouddriver.eureka.model.DataCenterInfo | ||
import com.netflix.spinnaker.clouddriver.eureka.model.DataCenterMetadata | ||
import com.netflix.spinnaker.clouddriver.eureka.model.EurekaApplication | ||
import com.netflix.spinnaker.clouddriver.eureka.model.EurekaApplications | ||
import com.netflix.spinnaker.clouddriver.eureka.model.EurekaInstance | ||
import com.netflix.spinnaker.clouddriver.model.HealthState | ||
import spock.lang.Specification | ||
|
||
import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.HEALTH | ||
import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.INSTANCES | ||
|
||
class EurekaCachingAgentSpec extends Specification { | ||
def providerCache = Stub(ProviderCache) | ||
def eurekaApi = Stub(EurekaApi) | ||
def eap = new TestEurekaAwareProvider() | ||
|
||
def agent = new EurekaCachingAgent(eurekaApi, "us-foo-2", new ObjectMapper(), "http://eureka", "true", "eureka-foo", [eap], 0, 0) | ||
|
||
def "it should cache instances"() { | ||
given: | ||
eurekaApi.loadEurekaApplications() >> new EurekaApplications(applications: [ | ||
new EurekaApplication(name: "foo", instances: [ | ||
instance("foo", "i-1", "UP"), | ||
instance("foo", "i-2", "UP") | ||
]) | ||
]) | ||
|
||
when: | ||
def result = agent.loadData(providerCache) | ||
|
||
then: | ||
result.cacheResults.size() == 2 | ||
result.cacheResults[HEALTH.ns].size() == 2 | ||
result.cacheResults[INSTANCES.ns].size() == 2 | ||
result.cacheResults[HEALTH.ns]*.id.sort() == ["us-foo-2:i-1:Discovery", "us-foo-2:i-2:Discovery"] | ||
result.cacheResults[INSTANCES.ns]*.id.sort() == ["us-foo-2:i-1", "us-foo-2:i-2"] | ||
} | ||
|
||
def "it should dedupe multiple discovery records prefering HealthState order"() { | ||
given: | ||
eurekaApi.loadEurekaApplications() >> new EurekaApplications(applications: [ | ||
new EurekaApplication(name: "foo", instances: [ | ||
instance("foo", "i-1", "UP"), | ||
instance("foo", "i-1", "DOWN") | ||
]) | ||
]) | ||
|
||
when: | ||
def result = agent.loadData(providerCache) | ||
|
||
then: | ||
result.cacheResults.size() == 2 | ||
result.cacheResults[HEALTH.ns].size() == 1 | ||
result.cacheResults[INSTANCES.ns].size() == 1 | ||
result.cacheResults[HEALTH.ns].first().attributes.state == HealthState.Down.name() | ||
|
||
} | ||
|
||
def "it should dedupe multiple discovery records preferring newest"() { | ||
given: | ||
eurekaApi.loadEurekaApplications() >> new EurekaApplications(applications: [ | ||
new EurekaApplication(name: "foo", instances: [ | ||
instance("foo", "i-1", "UP", 12345), | ||
instance("foo", "i-1", "UP", 23451), | ||
instance("foo", "i-1", "UP", 12344) | ||
]) | ||
]) | ||
|
||
when: | ||
def result = agent.loadData(providerCache) | ||
|
||
then: | ||
result.cacheResults.size() == 2 | ||
result.cacheResults[HEALTH.ns].size() == 1 | ||
result.cacheResults[INSTANCES.ns].size() == 1 | ||
result.cacheResults[HEALTH.ns].first().attributes.lastUpdatedTimestamp == 23451 | ||
|
||
} | ||
|
||
private static EurekaInstance instance(String app, String id, String status, Long timestamp = System.currentTimeMillis()) { | ||
EurekaInstance.buildInstance( | ||
"host", | ||
app, | ||
"127.0.0.1", | ||
status, | ||
"UNKNOWN", | ||
new DataCenterInfo( | ||
name: "my-dc", | ||
metadata: new DataCenterMetadata( | ||
accountId: "foo", | ||
availabilityZone: "us-foo-2a", | ||
amiId: "ami-foo", | ||
instanceId: id, | ||
instanceType: "m3.megabig")), | ||
"/status", | ||
"/healthcheck", | ||
id, | ||
id, | ||
timestamp, | ||
"$app-v000", | ||
null, | ||
id) | ||
} | ||
|
||
static class TestEurekaAwareProvider implements EurekaAwareProvider { | ||
@Override | ||
Boolean isProviderForEurekaRecord(Map<String, Object> attributes) { | ||
return true | ||
} | ||
|
||
@Override | ||
String getInstanceKey(Map<String, Object> attributes, String region) { | ||
return "$region:$attributes.instanceId" | ||
} | ||
|
||
@Override | ||
String getInstanceHealthKey(Map<String, Object> attributes, String region, String healthId) { | ||
return "$region:$attributes.instanceId:$healthId" | ||
} | ||
} | ||
} |