Skip to content

Shard key not honored in ReferenceLookupDelegate when DocumentReference resolves to a empty collection #4612

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

Closed
stefanbildl opened this issue Jan 12, 2024 · 10 comments
Assignees
Labels
type: bug A general bug

Comments

@stefanbildl
Copy link
Contributor

Shard key not honored in ReferenceLookupDelegate

I have identified an issue in org.springframework.data.mongodb.core.convert.ReferenceLookupDelegate when using sharded mongodb collections.

The shard key of our "Task" collection includes the fields "datacenter" and "flowId":

@Getter
@Setter
@Document(collection = "task", language = "none")
@AllArgsConstructor
@Sharded(shardKey = {"datacenter", "flowId"}, immutableKey = true)
public class Task {
....
}
  • We use a String field named "datacenter" that specifies where the data is stored (can be in one of our datacenters around the world)
  • e.g. If data is in Mexico, datacenter is "MEX", if data is in Shanghai, datacenter is "SHA", etc.
  • If we omit the datacenter field in any query, the query is run globally on every location (which is very slow)
  • If a location is not reachable global queries fail. This leads to extended downtime in our case!
@Getter
@Setter
@Document(collection = "task", language = "none")
@AllArgsConstructor
@Sharded(shardKey = {"datacenter", "flowId"}, immutableKey = true)
public class Task {
...
	@DocumentReference(lookup = "{'flowId': ?#{flowId}, 'datacenter': ?#{datacenter}}")
	@Indexed
	private Set<Task> dependsOn = new LinkedHashSet<>();
}

If "dependsOn" is a empty set, the ReferenceLookupDelegate method computeFilter
returns

ListDocumentReferenceQuery(NO_RESULTS_PREDICATE,...)

which - in our case - causes a global query. If a datacenter is currently unreachable, the query fails!

	DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) {
...
			if (objects.isEmpty()) {
				return new ListDocumentReferenceQuery(NO_RESULTS_PREDICATE, sort); // here is the Problem NO_RESULTS_PREDICATE is a query without datacenter field
			}
...

I don't understand, why this NO_RESULTS_PREDICATE is needed here. Is this just a way to say: query for a empty list?

Proposition

Instead of handling the empty list case in computeFilter, just add the following lines to readReference:

	@Nullable
	public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction,
			MongoEntityReader entityReader) {

		Object value = source instanceof DocumentReferenceSource documentReferenceSource ? documentReferenceSource.getTargetSource()
				: source;
 
                 // THIS IS NEW
		 if (property.isCollectionLike() && value instanceof Collection && ((Collection)value).isEmpty()) {
                      return new ArrayList<>();
                 }
                 // END OF NEW STUFF

                DocumentReferenceQuery filter = computeFilter(property, source, spELContext);

.....

Thank you very much!

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 12, 2024
@stefanbildl
Copy link
Contributor Author

Here is a hotfix for the problem:

Just use the following ReferenceResolver

    @Bean
    @Override
    public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, MongoCustomConversions customConversions, MongoMappingContext mappingContext) {
        DbRefResolver dbRefResolver = new MyReferenceResolver(databaseFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext);
        converter.setCustomConversions(customConversions);
        converter.setCodecRegistryProvider(databaseFactory);
        return converter;
    }



    private class MyReferenceResolver  extends DefaultDbRefResolver {
        public MyReferenceResolver(MongoDatabaseFactory mongoDbFactory) {
            super(mongoDbFactory);
        }

        @Override
        public Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
            if(source instanceof DocumentReferenceSource s && s.getTargetSource() instanceof Collection<?> c && c.isEmpty())  {
                return new ArrayList<>();
            }
            return super.resolveReference(property, source, referenceLookupDelegate, entityReader);
        }
    }

@christophstrobl
Copy link
Member

Thanks for reaching out and taking the time to provide a PR. We'll have a look.

@mp911de mp911de added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 22, 2024
@stefanbildl
Copy link
Contributor Author

Hi, any news on this issue?
Thank you.

@christophstrobl
Copy link
Member

@stefanbildl it's on the list - just not on the top of it.

christophstrobl pushed a commit that referenced this issue Mar 19, 2024
@christophstrobl
Copy link
Member

@stefanbildl we've slightly revised the proposed fix. You may want to have a look at the current issue branch.

@stefanbildl
Copy link
Contributor Author

@christophstrobl thanks, looks good to me. Your refactor makes it way easier to understand!

@stefanbildl
Copy link
Contributor Author

Oh, one thing I just noticed:

This is immutable: Collections.emptyList();

This could produce another bug where a user would try to add new entries to the empty list.
I intentionally used new ArrayList<>() for that purpose.

@stefanbildl
Copy link
Contributor Author

@christophstrobl please review this, thanks!

@stefanbildl
Copy link
Contributor Author

I think I get it now. the iterable is converted to the target type, am I right?

@christophstrobl
Copy link
Member

@stefanbildl this is the way! yes.

christophstrobl pushed a commit that referenced this issue Apr 10, 2024
@mp911de mp911de added this to the 4.2.5 (2023.1.5) milestone Apr 11, 2024
mp911de pushed a commit that referenced this issue Apr 11, 2024
Wrap no results predicate in dedicated lookup filter and make sure to omit loading attempt also for map properties.

See #4612
Original pull request: #4613
mp911de added a commit that referenced this issue Apr 11, 2024
Add Override annotations and comment why we optimize.

See #4612
Original pull request: #4613
mp911de pushed a commit that referenced this issue Apr 11, 2024
mp911de pushed a commit that referenced this issue Apr 11, 2024
Wrap no results predicate in dedicated lookup filter and make sure to omit loading attempt also for map properties.

See #4612
Original pull request: #4613
mp911de added a commit that referenced this issue Apr 11, 2024
Add Override annotations and comment why we optimize.

See #4612
Original pull request: #4613
natedanner pushed a commit to natedanner/spring-projects__spring-data-mongodb that referenced this issue May 20, 2024
natedanner pushed a commit to natedanner/spring-projects__spring-data-mongodb that referenced this issue May 20, 2024
Wrap no results predicate in dedicated lookup filter and make sure to omit loading attempt also for map properties.

See spring-projects#4612
Original pull request: spring-projects#4613
natedanner pushed a commit to natedanner/spring-projects__spring-data-mongodb that referenced this issue May 20, 2024
Add Override annotations and comment why we optimize.

See spring-projects#4612
Original pull request: spring-projects#4613
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
4 participants