Coder Social home page Coder Social logo

Resolver mixing up entities about knit-go HOT 7 CLOSED

go-aegian avatar go-aegian commented on July 25, 2024
Resolver mixing up entities

from knit-go.

Comments (7)

jhump avatar jhump commented on July 25, 2024

@go-aegian, this likely depends on your implementation of the ResolveCountryContinent RPC. The entries in the values field of the response must be in the same order as the inputs in the bases field of the request. So if there's anyway the responses could get out of order (any sorting, conversion to map and back, etc), that would do it.

from knit-go.

go-aegian avatar go-aegian commented on July 25, 2024

I'm using the resolver functions you have in swapi demo so maybe the resolveBatch is mixing it up

       results does have a different order than idSlice array so the code below is the problem

        indices := map[string]int{}
	for i, item := range idSlice {
		indices[item] = i
	}

func resolveBatch[E, R, M, W any](
	ctx context.Context,
	entities []E,
	limit int,
	idExtractor func(E) []string,
	invoker func(context.Context, []string) (*connect.Response[M], error),
	resultExtractor func(*M) []R,
	resultStorer func([]R, *W),
) ([]*W, error) {
	idSet := map[string]struct{}{}
	idBatches := make([][]string, len(entities))
	for i, entity := range entities {
		ids := idExtractor(entity)
		if limit > 0 && len(ids) > limit {
			ids = ids[:limit]
		}
		for _, item := range ids {
			idSet[item] = struct{}{}
		}
		idBatches[i] = ids
	}
	idSlice := make([]string, 0, len(idSet))
	for item := range idSet {
		idSlice = append(idSlice, item)
	}

	resp, err := invoker(ctx, idSlice)
	if err != nil {
		return nil, err
	}
	results := resultExtractor(resp.Msg)

	indices := map[string]int{}
	for i, item := range idSlice {
		indices[item] = i
	}
	batchedResults := make([]*W, len(entities))
	for i := range entities {
		ids := idBatches[i]
		batch := make([]R, len(ids))
		for j, item := range ids {
			batch[j] = results[indices[item]]
		}
		var w W
		resultStorer(batch, &w)
		batchedResults[i] = &w
	}
	return batchedResults, nil
}
func resolve1to1Batch[E, R, M, W any](
	ctx context.Context,
	entities []E,
	idExtractor func(E) string,
	invoker func(context.Context, []string) (*connect.Response[M], error),
	resultExtractor func(*M) []R,
	resultStorer func(R, *W),
) ([]*W, error) {
	return resolveBatch(
		ctx,
		entities,
		0,
		func(e E) []string {
			id := idExtractor(e)
			if id == "" {
				return nil
			}
			return []string{id}
		},
		invoker,
		resultExtractor,
		func(r []R, w *W) {
			if len(r) > 0 {
				resultStorer(r[0], w)
			}
		},
	)

from knit-go.

jhump avatar jhump commented on July 25, 2024

@go-aegian, there are several things that could go wrong there, too, depending on your implementation of the invoker and resultExtractor functions that you supply.

In particular, the results extracted from *connect.Response[M] response returned by the invoker must be in the same order as the input IDs, or else things will get mixed up.

I would recommend examining the requests and responses (possibly in a debugger) that your RPC handler is returning, and make sure things in the correct order. Once that is working, the knit relations should join the results correctly.

from knit-go.

go-aegian avatar go-aegian commented on July 25, 2024

@jhump that is correct assumption but should it be the resolver function to handle ordering correctly when matching input ids against response from the invoker, from my experience with graphql implementation they handle it internally?

from knit-go.

jhump avatar jhump commented on July 25, 2024

from my experience with graphql implementation they handle it internally

That is because graphQL requires a unique id field on every entity. But knit does not require that -- so that it can be used with existing Protobuf schemas and RPCs that do not contain that. So knit has no way of "automagically" correlating results with the input IDs.

Also, it is common in microservice architectures for the main entity to not know about IDs of its relations. Instead, a separate service will contains that other data and also contain the foreign relation knowledge. In the SWAPI example, this would be like the Planet entity not having a list of residents/person IDs. So the way to join in the residents of a planet is to ask the "Person" service for all people with a particular "homeworld" planetID. In this case, the framework (be it knit or graphql) cannot know how to join person to planet because it doesn't know which field to join on (in the example case, joining "person.homeworld_id" to "planet.id").

So, since there's not enough information in the schema alone to "explain" the relations and join criteria to the knit framework, it's up to the resolver.

from knit-go.

go-aegian avatar go-aegian commented on July 25, 2024

Well here it's the implementation I came up with that works

the ResolveMany guarantees ordering the idSlices prior to call invoker and results are then sorted using a custom delegate where they can be sorted by any field as the implementation requires.

This is generic and solves the problem, is there a way I can add this implementation to knit ?


func ResolveMany[E, R, M, W any](
	ctx context.Context,
	entities []E,
	limit int,
	idExtractor func(E) []string,
	invoker func(context.Context, []string) (*connect.Response[M], error),
	resultExtractor func(*M) []R,
	resultSorter func([]R) []R,
	resultHolder func([]R, *W),
) ([]*W, error) {
	idSet := map[string]struct{}{}
	idBatches := make([][]string, len(entities))
	
	for i, entity := range entities {
		ids := idExtractor(entity)
		if limit > 0 && len(ids) > limit {
			ids = ids[:limit]
		}
		for _, item := range ids {
			idSet[item] = struct{}{}
		}
		idBatches[i] = ids
	}
	
	idSlice := make([]string, 0, len(idSet))
	for item := range idSet {
		idSlice = append(idSlice, item)
	}
	
	batchedResults := make([]*W, len(entities))
	
	sort.Slice(idSlice, func(i, j int) bool {
		return idSlice[i] <= idSlice[j]
	})
	
	indexes := map[string]int{}
	for k, item := range idSlice {
		indexes[item] = k
	}
	
	resp, err := invoker(ctx, idSlice)
	if err != nil {
		return nil, err
	}
	
	results := resultExtractor(resp.Msg)
	if results == nil {
		return nil, fmt.Errorf("no data associated")
	}
	
	results = resultSorter(results)
	
	for i := range entities {
		ids := idBatches[i]
		batch := make([]R, len(ids))
		for j, item := range ids {
			batch[j] = results[indexes[item]]
		}
		
		var w W
		resultHolder(batch, &w)
		
		batchedResults[i] = &w
	}
	
	return batchedResults, nil
}
func ResolveOne[E, R, M, W any](
	ctx context.Context,	
	entities []E,
	idExtractor func(E) string,
	invoker func(context.Context, []string) (*connect.Response[M], error),
	resultExtractor func(*M) []R,
	resultSorter func([]R) []R,
	resultHolder func(R, *W),
) ([]*W, error) {
	return ResolveMany(
		ctx,
		entities,
		0,
		func(e E) []string {
			id := idExtractor(e)
			if id == "" {
				return nil
			}
			return []string{id}
		},
		invoker,
		resultExtractor,
		resultSorter,
		func(r []R, w *W) {
			if len(r) > 0 {
				resultHolder(r[0], w)
			}
		},
	)
}

from knit-go.

jhump avatar jhump commented on July 25, 2024

@go-aegian, the functions you based that on were private to the SWAPI demo because they were rather bespoke to the demo API and the regularity of its entities and relations. If we were to add a Go API to assist with implementing relations, we'd probably start over and try to come up with something more generic that was easier to use (and would likely support parallelism in resolving the elements in the batch).

We have not added any such thing to the Go API because it's unclear of its value -- a lot of organizations use gRPC but not Go, and the whole reason relations look the way they do is so they can be implemented in any language that supports gRPC. There were actually two previous iterations of how relations looked and how they were configured/implemented (before we made Knit open and public), and the main attraction of the current approach over the others is that an organization could adopt Knit without needing to worry about whether we had a library implemented in a particular language: if a language supports gRPC, you can use it with Knit.

So I'm afraid that, at this time, we're not yet considering adding this sort of thing to this repo. But feel free to file another issue, an enhancement request to add that kind of functionality, and we'll update that issue if/when our position changes.

from knit-go.

Related Issues (3)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.