From cf3f46f5c0ea6ba74aaac463af0bffd0a1a700ec Mon Sep 17 00:00:00 2001 From: Jakub Skoczen Date: Fri, 29 May 2026 07:47:27 +0200 Subject: [PATCH 1/2] Consider reciprocal flag on network when building rota --- broker/adapter/api_directory.go | 31 ++++++- broker/adapter/directory.go | 5 +- broker/test/adapter/api_directory_test.go | 106 ++++++++++++++++++++++ directory/directory_api.yaml | 2 + 4 files changed, 139 insertions(+), 5 deletions(-) diff --git a/broker/adapter/api_directory.go b/broker/adapter/api_directory.go index c3d6973e..7b125966 100644 --- a/broker/adapter/api_directory.go +++ b/broker/adapter/api_directory.go @@ -217,8 +217,9 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl suppTypeMatch := svcType == "" || svcType == strings.ToLower(suppTier.Type) suppLevelMatch := svcLevel == "" || svcLevel == strings.ToLower(suppTier.Level) suppCostMatch := costMatches(suppTier.Cost, maxCost) + suppNetworkMatch := tierMatchesSharedNetwork(suppTier.Cost, reqNetworks, supNetworks) - if suppTypeMatch && suppLevelMatch && suppCostMatch { + if suppTypeMatch && suppLevelMatch && suppCostMatch && suppNetworkMatch { reciprocal := true //supplier tier matched the request, if the tier is free it must be reciprocal if suppTier.Cost == 0 { @@ -289,6 +290,29 @@ func costMatches(suppCost, maxCost float64) bool { } } +func tierMatchesSharedNetwork(suppCost float64, reqNetworks, supNetworks map[string]Network) bool { + for name, reqNet := range reqNetworks { + supNet, ok := supNetworks[name] + if !ok { + continue + } + if networkAllowsCost(reqNet, suppCost) && networkAllowsCost(supNet, suppCost) { + return true + } + } + return false +} + +func networkAllowsCost(network Network, cost float64) bool { + if network.Reciprocal == nil { + return true + } + if *network.Reciprocal { + return cost == 0 + } + return cost > 0 +} + func CompareSuppliers(a, b SupplierOrdering) int { if a.IsLocal() && !b.IsLocal() { return -1 @@ -316,8 +340,9 @@ func getPeerNetworks(peerData directory.Entry) map[string]Network { for _, n := range *peerData.Networks { if n.Priority != nil { networks[n.Name] = Network{ - Name: n.Name, - Priority: int(*n.Priority), + Name: n.Name, + Priority: int(*n.Priority), + Reciprocal: n.Reciprocal, } } } diff --git a/broker/adapter/directory.go b/broker/adapter/directory.go index 77630de6..5c69963a 100644 --- a/broker/adapter/directory.go +++ b/broker/adapter/directory.go @@ -59,8 +59,9 @@ func (s Supplier) IsLocal() bool { return s.Local } func (s Supplier) GetRatio() float32 { return s.Ratio } type Network struct { - Name string `json:"name"` - Priority int `json:"priority"` + Name string `json:"name"` + Priority int `json:"priority"` + Reciprocal *bool `json:"reciprocal,omitempty"` } type Tier struct { diff --git a/broker/test/adapter/api_directory_test.go b/broker/test/adapter/api_directory_test.go index 066fe427..12be3555 100644 --- a/broker/test/adapter/api_directory_test.go +++ b/broker/test/adapter/api_directory_test.go @@ -33,6 +33,22 @@ func createDirectoryAdapter(urls ...string) adapter.DirectoryLookupAdapter { return adapter.CreateApiDirectory(http.DefaultClient, urls) } +func boolPtr(v bool) *bool { + return &v +} + +func withNetworkReciprocal(entry directory.Entry, reciprocal *bool) directory.Entry { + if entry.Networks == nil { + return entry + } + networks := slices.Clone(*entry.Networks) + for i := range networks { + networks[i].Reciprocal = reciprocal + } + entry.Networks = &networks + return entry +} + func TestGetVendorFromUrl(t *testing.T) { tests := []struct { name string @@ -495,6 +511,96 @@ func TestFilterAndSortReciprocal(t *testing.T) { assert.Equal(t, "copy", rotaInfo.Request.Type) } +func TestFilterAndSortReciprocalNetworkExcludesPaidTiers(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterData := withNetworkReciprocal(dirEntries.Items[0], boolPtr(true)) + entries := []adapter.Supplier{ + {PeerId: "2", Ratio: 0.7, Symbol: "AU-NU", CustomData: dirEntries.Items[2]}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Core", + }, + ServiceType: iso18626.TypeServiceTypeLoan, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 3500, + Exp: 2, + }, + }, + } + + entries, rotaInfo := ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Empty(t, entries) + assert.Len(t, rotaInfo.Suppliers, 1) + assert.False(t, rotaInfo.Suppliers[0].Match) +} + +func TestFilterAndSortPaidNetworkExcludesFreeTiers(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterData := withNetworkReciprocal(dirEntries.Items[4], boolPtr(false)) + entries := []adapter.Supplier{ + {PeerId: "3", Ratio: 0.7, Symbol: "AU-VVWA", CustomData: dirEntries.Items[4]}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Rush", + }, + ServiceType: iso18626.TypeServiceTypeCopy, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 0, + Exp: 2, + }, + }, + } + + entries, rotaInfo := ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Empty(t, entries) + assert.Len(t, rotaInfo.Suppliers, 1) + assert.False(t, rotaInfo.Suppliers[0].Match) +} + +func TestFilterAndSortPaidNetworkAllowsPaidTiers(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterData := withNetworkReciprocal(dirEntries.Items[0], boolPtr(false)) + entries := []adapter.Supplier{ + {PeerId: "2", Ratio: 0.7, Symbol: "AU-NU", CustomData: withNetworkReciprocal(dirEntries.Items[2], boolPtr(false))}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Core", + }, + ServiceType: iso18626.TypeServiceTypeLoan, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 3500, + Exp: 2, + }, + }, + } + + entries, rotaInfo := ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Len(t, entries, 1) + assert.Equal(t, "2", entries[0].PeerId) + assert.Equal(t, 34.4, entries[0].Cost) + assert.Len(t, rotaInfo.Suppliers, 1) + assert.True(t, rotaInfo.Suppliers[0].Match) + assert.Equal(t, "34.40", rotaInfo.Suppliers[0].Cost) +} + func TestFilterAndSortNoFilters(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) ad := createDirectoryAdapter("") diff --git a/directory/directory_api.yaml b/directory/directory_api.yaml index 12294c69..50bebbfd 100644 --- a/directory/directory_api.yaml +++ b/directory/directory_api.yaml @@ -144,6 +144,8 @@ components: type: string priority: type: integer + reciprocal: + type: boolean Membership: type: object required: From d23139ae1f9c690da0d6e2075553545b5687f7f0 Mon Sep 17 00:00:00 2001 From: Jakub Skoczen Date: Fri, 29 May 2026 11:09:23 +0200 Subject: [PATCH 2/2] Honor network priority --- broker/adapter/api_directory.go | 27 +++++++----- broker/test/adapter/api_directory_test.go | 51 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/broker/adapter/api_directory.go b/broker/adapter/api_directory.go index 7b125966..78894dc4 100644 --- a/broker/adapter/api_directory.go +++ b/broker/adapter/api_directory.go @@ -176,11 +176,11 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl Match: false, }) } - priority := math.MaxInt + sharedPriority := math.MaxInt for name, reqNet := range reqNetworks { if _, ok := supNetworks[name]; ok { - if priority > reqNet.Priority { - priority = reqNet.Priority + if sharedPriority > reqNet.Priority { + sharedPriority = reqNet.Priority } for i, n := range supMatch.Networks { if n.Name == name { @@ -202,11 +202,12 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl } return cmp.Compare(a.Name, b.Name) }) - if priority < math.MaxInt { - sup.Priority = priority + if sharedPriority < math.MaxInt { + sup.Priority = sharedPriority suppTiers := getPeerTiers(sup.CustomData) supMatch.Tiers = make([]TierMatch, 0, len(suppTiers)) cost := math.MaxFloat64 + priority := math.MaxInt for _, suppTier := range suppTiers { var tierMatch TierMatch tierMatch.Name = suppTier.Name @@ -217,7 +218,8 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl suppTypeMatch := svcType == "" || svcType == strings.ToLower(suppTier.Type) suppLevelMatch := svcLevel == "" || svcLevel == strings.ToLower(suppTier.Level) suppCostMatch := costMatches(suppTier.Cost, maxCost) - suppNetworkMatch := tierMatchesSharedNetwork(suppTier.Cost, reqNetworks, supNetworks) + tierPriority := sharedNetworkPriorityForCost(suppTier.Cost, reqNetworks, supNetworks) + suppNetworkMatch := tierPriority < math.MaxInt if suppTypeMatch && suppLevelMatch && suppCostMatch && suppNetworkMatch { reciprocal := true @@ -235,8 +237,9 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl } } tierMatch.Match = reciprocal - if reciprocal && cost > suppTier.Cost { + if reciprocal && (cost > suppTier.Cost || cost == suppTier.Cost && priority > tierPriority) { cost = suppTier.Cost + priority = tierPriority } } supMatch.Tiers = append(supMatch.Tiers, tierMatch) @@ -258,6 +261,7 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl supMatch.Match = true supMatch.Cost = fmt.Sprintf("%.2f", cost) sup.Cost = cost + sup.Priority = priority filtered = append(filtered, sup) } supMatch.Priority = sup.Priority @@ -290,17 +294,20 @@ func costMatches(suppCost, maxCost float64) bool { } } -func tierMatchesSharedNetwork(suppCost float64, reqNetworks, supNetworks map[string]Network) bool { +func sharedNetworkPriorityForCost(suppCost float64, reqNetworks, supNetworks map[string]Network) int { + priority := math.MaxInt for name, reqNet := range reqNetworks { supNet, ok := supNetworks[name] if !ok { continue } if networkAllowsCost(reqNet, suppCost) && networkAllowsCost(supNet, suppCost) { - return true + if priority > reqNet.Priority { + priority = reqNet.Priority + } } } - return false + return priority } func networkAllowsCost(network Network, cost float64) bool { diff --git a/broker/test/adapter/api_directory_test.go b/broker/test/adapter/api_directory_test.go index 12be3555..55521fab 100644 --- a/broker/test/adapter/api_directory_test.go +++ b/broker/test/adapter/api_directory_test.go @@ -37,6 +37,10 @@ func boolPtr(v bool) *bool { return &v } +func intPtr(v int) *int { + return &v +} + func withNetworkReciprocal(entry directory.Entry, reciprocal *bool) directory.Entry { if entry.Networks == nil { return entry @@ -601,6 +605,53 @@ func TestFilterAndSortPaidNetworkAllowsPaidTiers(t *testing.T) { assert.Equal(t, "34.40", rotaInfo.Suppliers[0].Cost) } +func TestFilterAndSortUsesCompatibleNetworkPriority(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterNetworks := []directory.Network{ + {Name: "Reciprocal", Priority: intPtr(1), Reciprocal: boolPtr(true)}, + {Name: "Paid Low", Priority: intPtr(5), Reciprocal: boolPtr(false)}, + {Name: "Paid High", Priority: intPtr(3), Reciprocal: boolPtr(false)}, + } + paidTier := []directory.Tier{ + {Name: "Paid Core Loan", Level: "Core", Type: "Loan", Cost: 34.4}, + } + requesterData := directory.Entry{Name: "Requester", Networks: &requesterNetworks} + supplierANetworks := []directory.Network{ + {Name: "Reciprocal", Priority: intPtr(1), Reciprocal: boolPtr(true)}, + {Name: "Paid Low", Priority: intPtr(5), Reciprocal: boolPtr(false)}, + } + supplierBNetworks := []directory.Network{ + {Name: "Paid High", Priority: intPtr(3), Reciprocal: boolPtr(false)}, + } + entries := []adapter.Supplier{ + {PeerId: "A", Symbol: "A", CustomData: directory.Entry{Name: "Supplier A", Networks: &supplierANetworks, Tiers: &paidTier}}, + {PeerId: "B", Symbol: "B", CustomData: directory.Entry{Name: "Supplier B", Networks: &supplierBNetworks, Tiers: &paidTier}}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Core", + }, + ServiceType: iso18626.TypeServiceTypeLoan, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 3500, + Exp: 2, + }, + }, + } + + entries, _ = ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Len(t, entries, 2) + assert.Equal(t, "B", entries[0].PeerId) + assert.Equal(t, 3, entries[0].Priority) + assert.Equal(t, "A", entries[1].PeerId) + assert.Equal(t, 5, entries[1].Priority) +} + func TestFilterAndSortNoFilters(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) ad := createDirectoryAdapter("")