Comments (7)
@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.
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.
@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.
@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.
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.
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.
@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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from knit-go.