Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MapPropertySource changes are not reflected in the adapted ConfigurationPropertySource #13344

Conversation

fahimfarookme
Copy link
Contributor

@fahimfarookme fahimfarookme commented Jun 3, 2018

Issue

In spring-boot 2x Maps in @ConfigurationProperties are not being updated from the second EnvironmentChangeEvent onwards. i.e. Following test fails in spring-boot 2x, but passed in spring-boot 1x.

@ConfigurationProperties
class Props {
   private Map<String, String> map = new HashMap<>();
   ...
}


TestPropertyValues.of("map.key1=value1").applyTo(this.environment);

// first EnvironmentChangeEvent
context.publishEvent(new EnvironmentChangeEvent(..."map.key1"));
// 'value1' gets reflected in @ConfigurationProperties bean - in both spring-boot 1x and 2x
assertEquals("value1", context.getBean(Props.class).getMap().get("key1"));

TestPropertyValues.of("map.key2=value2").applyTo(this.environment);

// second EnvironmentChangeEvent
context.publishEvent(new EnvironmentChangeEvent(..."map.key2"));

// 'value2' NOT getting reflected in @ConfigurationProperties bean - in spring-boot 2x.
assertEquals("value2", context.getBean(Props.class).getMap().get("key2"));

Analysis

  • In case of MapPropertySource, SpringIterableConfigurationPropertySource#getCacheKey()
    here always returns the reference to the same object in the heap (for different invocations), even though the property-source state has changed. i.e.

    return ((MapPropertySource) getPropertySource()).getSource().keySet();

  • As a result, if (ObjectUtils.nullSafeEquals(cacheKey, this.cacheKey)) here always evaluates to true (same reference) and getCache() returns the old/ initial cache.

Consequently, SpringIterableConfigurationPropertySource#iterator() having reference only to the initial/ old property-keys - hence the MapBinder fails here to bind updated properties.

Fix

SpringIterableConfigurationPropertySource#getCacheKey() should return a new object. This branch - if (getPropertySource() instanceof MapPropertySource) is not necessary.

private Object getCacheKey() {
   return getPropertySource().getPropertyNames(); // returns a new array
}

Whenever the cacheKey is updated, Cache is reinitialized (with all the property keys)

Sample

  • PoC here to confirm/ recreate the issue - the same set of test cases passed here (spring-boot 1x), but failed here (spring-boot 2x).

This issue supersedes this one.

In spring-boot 2x `Maps` in `@ConfigurationProperties` are not being updated from the **second** `EnvironmentChangeEvent` onwards. i.e. Following test fails in spring-boot 2x, but passed in spring-boot 1x.

- In case of `MapPropertySource`, `SpringIterableConfigurationPropertySource#getCacheKey()`
[here](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java#L146) always returns the reference to the same object in the heap (for different invocations), even though the property-source state has changed. i.e. <br/>
   `return ((MapPropertySource) getPropertySource()).getSource().keySet();`

- As a result, `if (ObjectUtils.nullSafeEquals(cacheKey, this.cacheKey))` [here](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java#L136) always evaluates to true (same reference) and `getCache()` returns the old/ initial cache.

Consequently, `SpringIterableConfigurationPropertySource#iterator()` having reference only to the initial/ old property-keys - hence the `MapBinder` fails [here](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java#L152) to bind updated properties.
@@ -500,12 +501,23 @@ public void systemPropertiesAreSetForLoggingConfiguration() {
@Test
Copy link
Contributor Author

@fahimfarookme fahimfarookme Jun 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below test case was passing before due to a side-effect of the issue I reported in SpringIterableConfigurationPropertySource. ${pid} was resolvable.

@philwebb philwebb added type: bug A general bug for: merge-with-amendments Needs some changes when we merge and removed status: waiting-for-triage An issue we've not yet triaged labels Jun 4, 2018
@philwebb
Copy link
Member

philwebb commented Jun 4, 2018

Thanks for pull-request and the detailed analysis. I need to look in a a bit more detail to see what's going on before merging this. The use of keySet for MapPropertySource was an intentional optimization designed to prevent unnecessary creation of String[]. I'm worried that removing this might cause performance problems.

@philwebb philwebb added this to the 2.0.x milestone Jun 4, 2018
@spencergibb
Copy link
Member

@philwebb I just want to point out that there are failures in spring cloud if you didn't notice the link above spring-cloud/spring-cloud-netflix#2538

@philwebb
Copy link
Member

philwebb commented Jun 4, 2018

@spencergibb Thanks! We fixed something around this before didn't we?

@spencergibb
Copy link
Member

spencergibb commented Jun 4, 2018

Indeed it was a refresh that only happened on the first request. The key wasn't checked because the property source was new.

@philwebb philwebb self-assigned this Jun 6, 2018
philwebb pushed a commit that referenced this pull request Jun 6, 2018
Update `SpringIterableConfigurationPropertySource` so that the cache
key from a `MapPropertySource` is invalidated when the map contents
changes.

Prior to this commit, the actual keys of the map were used as the key.
This meant that if the underlying map changed, they key wouldn't be
invalidated because it ultimately pointed to the same object instance.

See gh-13344
philwebb added a commit that referenced this pull request Jun 6, 2018
* pr/13344:
  Polish "Fix caching issues with map property sources"
  Fix caching issues with map property sources
@philwebb philwebb closed this in dc1c459 Jun 6, 2018
@philwebb
Copy link
Member

philwebb commented Jun 6, 2018

@fahimfarookme Thanks very much for the pull-request. I've merged this into 2.0.x and master with an additional commit that should hopefully mean we keep the performance characteristics of the previous version.

@philwebb philwebb modified the milestones: 2.0.x, 2.0.3 Jun 6, 2018
@philwebb philwebb changed the title Support binding Maps with updated MapPropertySource MapPropertySource changes are not reflected in the adapted ConfigurationPropertySource Jun 6, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: merge-with-amendments Needs some changes when we merge type: bug A general bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants