Skip to content

Commit 0f2ffdc

Browse files
authored
Fix UpdateBalancingState not called when address attributes are modified (#2553)
1 parent 490894c commit 0f2ffdc

File tree

7 files changed

+212
-10
lines changed

7 files changed

+212
-10
lines changed

Diff for: examples/Container/Backend/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mcr.microsoft.com/dotnet/nightly/sdk:9.0-preview AS build-env
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env
22
WORKDIR /app
33

44
# Copy everything
@@ -8,7 +8,7 @@ RUN dotnet restore examples/Container/Backend
88
RUN dotnet publish examples/Container/Backend -c Release -o out
99

1010
# Build runtime image
11-
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0-preview
11+
FROM mcr.microsoft.com/dotnet/aspnet:9.0
1212
WORKDIR /app
1313
COPY --from=build-env /app/out .
1414
ENTRYPOINT ["dotnet", "Backend.dll"]

Diff for: examples/Container/Frontend/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mcr.microsoft.com/dotnet/nightly/sdk:9.0-preview AS build-env
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env
22
WORKDIR /app
33

44
# Copy everything
@@ -8,7 +8,7 @@ RUN dotnet restore examples/Container/Frontend
88
RUN dotnet publish examples/Container/Frontend -c Release -o out
99

1010
# Build runtime image
11-
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0-preview
11+
FROM mcr.microsoft.com/dotnet/aspnet:9.0
1212
WORKDIR /app
1313
COPY --from=build-env /app/out .
1414
ENTRYPOINT ["dotnet", "Frontend.dll"]

Diff for: global.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "9.0.100-preview.7.24407.12",
3+
"version": "9.0.100-rc.2.24474.11",
44
"rollForward": "latestFeature"
55
}
66
}

Diff for: src/Grpc.Net.Client/Balancer/SubchannelsLoadBalancer.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ public override void UpdateChannelState(ChannelState state)
122122

123123
var allUpdatedSubchannels = new List<AddressSubchannel>();
124124
var newSubchannels = new List<Subchannel>();
125+
var hasModifiedSubchannels = false;
125126
var currentSubchannels = _addressSubchannels.ToList();
126127

127128
// The state's addresses is the new authoritative list of addresses.
@@ -150,6 +151,8 @@ public override void UpdateChannelState(ChannelState state)
150151
address,
151152
newOrCurrentSubchannel.LastKnownState);
152153
newOrCurrentSubchannel.Subchannel.UpdateAddresses(new[] { address });
154+
155+
hasModifiedSubchannels = true;
153156
}
154157

155158
SubchannelLog.SubchannelPreserved(_logger, newOrCurrentSubchannel.Subchannel.Id, address);
@@ -171,7 +174,7 @@ public override void UpdateChannelState(ChannelState state)
171174
// This can all be removed.
172175
var removedSubConnections = currentSubchannels;
173176

174-
if (removedSubConnections.Count == 0 && newSubchannels.Count == 0)
177+
if (removedSubConnections.Count == 0 && newSubchannels.Count == 0 && !hasModifiedSubchannels)
175178
{
176179
SubchannelsLoadBalancerLog.ConnectionsUnchanged(_logger);
177180
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
#if SUPPORT_LOAD_BALANCING
20+
using System.Net;
21+
using Grpc.Core;
22+
using Grpc.Net.Client.Balancer;
23+
using Grpc.Net.Client.Balancer.Internal;
24+
using Grpc.Tests.Shared;
25+
using Microsoft.Extensions.Logging;
26+
using Microsoft.Extensions.Logging.Abstractions;
27+
28+
namespace Grpc.Net.Client.Tests.Infrastructure.Balancer;
29+
30+
[TestFixture]
31+
public class SubchannelsLoadBalancerTests
32+
{
33+
[Test]
34+
public void UpdateChannelState_AddressMatchAndAttributesDifferent_UpdateState()
35+
{
36+
// Arrange
37+
const string host1 = "127.0.0.1";
38+
const string host2 = "127.0.0.2";
39+
const int port = 80;
40+
41+
const string attributeKey = "key1";
42+
43+
var controller = new CustomChannelControlHelper();
44+
var balancer = new CustomBalancer(controller, NullLoggerFactory.Instance);
45+
46+
// create 2 addresses with some attributes
47+
var address1 = new BalancerAddress(host1, port);
48+
address1.Attributes.TryAdd(attributeKey, 20); // <-- difference
49+
50+
var address2 = new BalancerAddress(host2, port);
51+
address2.Attributes.TryAdd(attributeKey, 80); // <-- difference
52+
53+
var state1 = new ChannelState(
54+
status: new Status(),
55+
addresses: [address1, address2],
56+
loadBalancingConfig: null,
57+
attributes: new BalancerAttributes());
58+
59+
// create 2 addresses with the same hosts and ports as previous but with other attribute values
60+
var address3 = new BalancerAddress(host1, port);
61+
address3.Attributes.TryAdd(attributeKey, 40); // <-- difference
62+
63+
var address4 = new BalancerAddress(host2, port);
64+
address4.Attributes.TryAdd(attributeKey, 60); // <-- difference
65+
66+
var state2 = new ChannelState(
67+
status: new Status(),
68+
addresses: [address3, address4],
69+
loadBalancingConfig: null,
70+
attributes: new BalancerAttributes());
71+
72+
// Act
73+
// first update with `address1` and `address2`
74+
balancer.UpdateChannelState(state1);
75+
76+
// remember count of `IChannelControlHelper.UpdateState()` calls
77+
var updateStateCallsCount1 = controller.UpdateStateCallsCount;
78+
79+
// second update with `address3` and `address4`
80+
// which differs from `address1` and `address2` _only_ in attributes values
81+
balancer.UpdateChannelState(state2);
82+
83+
// get count of `IChannelControlHelper.UpdateState()` calls after second update
84+
var updateStateCallsCount2 = controller.UpdateStateCallsCount;
85+
86+
// Assert
87+
Assert.True(
88+
updateStateCallsCount2 > updateStateCallsCount1,
89+
"`IChannelControlHelper.UpdateState()` was not called from `SubchannelsLoadBalancer.UpdateChannelState()`");
90+
}
91+
}
92+
93+
file class CustomBalancer(
94+
IChannelControlHelper controller,
95+
ILoggerFactory loggerFactory)
96+
: SubchannelsLoadBalancer(controller, loggerFactory)
97+
{
98+
protected override SubchannelPicker CreatePicker(IReadOnlyList<Subchannel> readySubchannels)
99+
{
100+
return new CustomPicker(readySubchannels);
101+
}
102+
}
103+
104+
file class CustomPicker : SubchannelPicker
105+
{
106+
private IReadOnlyList<Subchannel> readySubchannels;
107+
108+
public CustomPicker(IReadOnlyList<Subchannel> readySubchannels)
109+
{
110+
this.readySubchannels = readySubchannels;
111+
}
112+
113+
public override PickResult Pick(PickContext context)
114+
{
115+
return PickResult.ForSubchannel(readySubchannels[0]);
116+
}
117+
}
118+
119+
file class CustomChannelControlHelper : IChannelControlHelper
120+
{
121+
public int UpdateStateCallsCount { get; private set; }
122+
123+
public Subchannel CreateSubchannel(SubchannelOptions options)
124+
{
125+
var subchannelTransportFactory = new CustomSubchannelTransportFactory();
126+
127+
var manager = new ConnectionManager(
128+
new CustomResolver(),
129+
true,
130+
NullLoggerFactory.Instance,
131+
new CustomBackoffPolicyFactory(),
132+
subchannelTransportFactory,
133+
[]);
134+
135+
return ((IChannelControlHelper)manager).CreateSubchannel(options);
136+
}
137+
138+
public void UpdateState(BalancerState state)
139+
{
140+
UpdateStateCallsCount++;
141+
}
142+
143+
public void RefreshResolver() { }
144+
}
145+
146+
file class CustomResolver() : PollingResolver(NullLoggerFactory.Instance)
147+
{
148+
protected override Task ResolveAsync(CancellationToken cancellationToken)
149+
{
150+
return Task.CompletedTask;
151+
}
152+
}
153+
154+
file class CustomBackoffPolicyFactory : IBackoffPolicyFactory
155+
{
156+
public IBackoffPolicy Create()
157+
{
158+
return new CustomBackoffPolicy();
159+
}
160+
}
161+
162+
file class CustomBackoffPolicy : IBackoffPolicy
163+
{
164+
public TimeSpan NextBackoff()
165+
{
166+
return TimeSpan.Zero;
167+
}
168+
}
169+
170+
file class CustomSubchannelTransportFactory : ISubchannelTransportFactory
171+
{
172+
public ISubchannelTransport Create(Subchannel subchannel)
173+
{
174+
return new CustomSubchannelTransport();
175+
}
176+
}
177+
178+
file class CustomSubchannelTransport : ISubchannelTransport
179+
{
180+
public void Dispose() { }
181+
182+
public DnsEndPoint? CurrentEndPoint { get; }
183+
public TimeSpan? ConnectTimeout { get; }
184+
public TransportStatus TransportStatus { get; }
185+
186+
public ValueTask<Stream> GetStreamAsync(DnsEndPoint endPoint, CancellationToken cancellationToken)
187+
{
188+
return ValueTask.FromResult<Stream>(new MemoryStream());
189+
}
190+
191+
public ValueTask<ConnectResult> TryConnectAsync(ConnectContext context, int attempt)
192+
{
193+
return ValueTask.FromResult(ConnectResult.Success);
194+
}
195+
196+
public void Disconnect() { }
197+
}
198+
199+
#endif

Diff for: testassets/InteropTestsGrpcWebWebsite/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mcr.microsoft.com/dotnet/nightly/sdk:9.0-preview AS build-env
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env
22
WORKDIR /app
33

44
# Copy everything
@@ -8,7 +8,7 @@ RUN dotnet restore testassets/InteropTestsGrpcWebWebsite
88
RUN dotnet publish testassets/InteropTestsGrpcWebWebsite -c Release -o out
99

1010
# Build runtime image
11-
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0-preview
11+
FROM mcr.microsoft.com/dotnet/aspnet:9.0
1212
WORKDIR /app
1313
COPY --from=build-env /app/out .
1414
ENTRYPOINT ["dotnet", "InteropTestsGrpcWebWebsite.dll", "--urls", "http://+:80"]

Diff for: testassets/InteropTestsWebsite/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mcr.microsoft.com/dotnet/nightly/sdk:9.0-preview AS build-env
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env
22
WORKDIR /app
33

44
# Copy everything
@@ -8,7 +8,7 @@ RUN dotnet restore testassets/InteropTestsWebsite
88
RUN dotnet publish testassets/InteropTestsWebsite --framework net9.0 -c Release -o out -p:LatestFramework=true
99

1010
# Build runtime image
11-
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0-preview
11+
FROM mcr.microsoft.com/dotnet/aspnet:9.0
1212
WORKDIR /app
1313
COPY --from=build-env /app/out .
1414
ENTRYPOINT ["dotnet", "InteropTestsWebsite.dll", "--port_http1", "80"]

0 commit comments

Comments
 (0)