|
|
@ -39,7 +39,7 @@ namespace Grpc.IntegrationTesting |
|
|
|
|
|
|
|
|
|
|
|
[Option("qps", Default = 1)] |
|
|
|
[Option("qps", Default = 1)] |
|
|
|
|
|
|
|
|
|
|
|
// The desired QPS per channel. |
|
|
|
// The desired QPS per channel, for each type of RPC. |
|
|
|
public int Qps { get; set; } |
|
|
|
public int Qps { get; set; } |
|
|
|
|
|
|
|
|
|
|
|
[Option("server", Default = "localhost:8080")] |
|
|
|
[Option("server", Default = "localhost:8080")] |
|
|
@ -53,18 +53,37 @@ namespace Grpc.IntegrationTesting |
|
|
|
|
|
|
|
|
|
|
|
[Option("print_response", Default = false)] |
|
|
|
[Option("print_response", Default = false)] |
|
|
|
public bool PrintResponse { get; set; } |
|
|
|
public bool PrintResponse { get; set; } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Types of RPCs to make, ',' separated string. RPCs can be EmptyCall or UnaryCall |
|
|
|
|
|
|
|
[Option("rpc", Default = "UnaryCall")] |
|
|
|
|
|
|
|
public string Rpc { get; set; } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The metadata to send with each RPC, in the format EmptyCall:key1:value1,UnaryCall:key2:value2 |
|
|
|
|
|
|
|
[Option("metadata", Default = null)] |
|
|
|
|
|
|
|
public string Metadata { get; set; } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal enum RpcType |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
UnaryCall, |
|
|
|
|
|
|
|
EmptyCall |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ClientOptions options; |
|
|
|
ClientOptions options; |
|
|
|
|
|
|
|
|
|
|
|
StatsWatcher statsWatcher = new StatsWatcher(); |
|
|
|
StatsWatcher statsWatcher = new StatsWatcher(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
List<RpcType> rpcs; |
|
|
|
|
|
|
|
Dictionary<RpcType, Metadata> metadata; |
|
|
|
|
|
|
|
|
|
|
|
// make watcher accessible by tests |
|
|
|
// make watcher accessible by tests |
|
|
|
internal StatsWatcher StatsWatcher => statsWatcher; |
|
|
|
internal StatsWatcher StatsWatcher => statsWatcher; |
|
|
|
|
|
|
|
|
|
|
|
internal XdsInteropClient(ClientOptions options) |
|
|
|
internal XdsInteropClient(ClientOptions options) |
|
|
|
{ |
|
|
|
{ |
|
|
|
this.options = options; |
|
|
|
this.options = options; |
|
|
|
|
|
|
|
this.rpcs = ParseRpcArgument(this.options.Rpc); |
|
|
|
|
|
|
|
this.metadata = ParseMetadataArgument(this.options.Metadata); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static void Run(string[] args) |
|
|
|
public static void Run(string[] args) |
|
|
@ -124,8 +143,11 @@ namespace Grpc.IntegrationTesting |
|
|
|
var stopwatch = Stopwatch.StartNew(); |
|
|
|
var stopwatch = Stopwatch.StartNew(); |
|
|
|
while (!cancellationToken.IsCancellationRequested) |
|
|
|
while (!cancellationToken.IsCancellationRequested) |
|
|
|
{ |
|
|
|
{ |
|
|
|
inflightTasks.Add(RunSingleRpcAsync(client, cancellationToken)); |
|
|
|
foreach (var rpcType in rpcs) |
|
|
|
rpcsStarted++; |
|
|
|
{ |
|
|
|
|
|
|
|
inflightTasks.Add(RunSingleRpcAsync(client, cancellationToken, rpcType)); |
|
|
|
|
|
|
|
rpcsStarted++; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// only cleanup calls that have already completed, calls that are still inflight will be cleaned up later. |
|
|
|
// only cleanup calls that have already completed, calls that are still inflight will be cleaned up later. |
|
|
|
await CleanupCompletedTasksAsync(inflightTasks); |
|
|
|
await CleanupCompletedTasksAsync(inflightTasks); |
|
|
@ -133,7 +155,7 @@ namespace Grpc.IntegrationTesting |
|
|
|
Console.WriteLine($"Currently {inflightTasks.Count} in-flight RPCs"); |
|
|
|
Console.WriteLine($"Currently {inflightTasks.Count} in-flight RPCs"); |
|
|
|
|
|
|
|
|
|
|
|
// if needed, wait a bit before we start the next RPC. |
|
|
|
// if needed, wait a bit before we start the next RPC. |
|
|
|
int nextDueInMillis = (int) Math.Max(0, (1000 * rpcsStarted / options.Qps) - stopwatch.ElapsedMilliseconds); |
|
|
|
int nextDueInMillis = (int) Math.Max(0, (1000 * rpcsStarted / options.Qps / rpcs.Count) - stopwatch.ElapsedMilliseconds); |
|
|
|
if (nextDueInMillis > 0) |
|
|
|
if (nextDueInMillis > 0) |
|
|
|
{ |
|
|
|
{ |
|
|
|
await Task.Delay(nextDueInMillis); |
|
|
|
await Task.Delay(nextDueInMillis); |
|
|
@ -146,25 +168,61 @@ namespace Grpc.IntegrationTesting |
|
|
|
Console.WriteLine($"Channel shutdown {channelId}"); |
|
|
|
Console.WriteLine($"Channel shutdown {channelId}"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async Task RunSingleRpcAsync(TestService.TestServiceClient client, CancellationToken cancellationToken) |
|
|
|
private async Task RunSingleRpcAsync(TestService.TestServiceClient client, CancellationToken cancellationToken, RpcType rpcType) |
|
|
|
{ |
|
|
|
{ |
|
|
|
long rpcId = statsWatcher.RpcIdGenerator.Increment(); |
|
|
|
long rpcId = statsWatcher.RpcIdGenerator.Increment(); |
|
|
|
try |
|
|
|
try |
|
|
|
{ |
|
|
|
{ |
|
|
|
Console.WriteLine($"Starting RPC {rpcId}."); |
|
|
|
Console.WriteLine($"Starting RPC {rpcId} of type {rpcType}"); |
|
|
|
var response = await client.UnaryCallAsync(new SimpleRequest(), |
|
|
|
|
|
|
|
new CallOptions(cancellationToken: cancellationToken, deadline: DateTime.UtcNow.AddSeconds(options.RpcTimeoutSec))); |
|
|
|
// metadata to send with the RPC |
|
|
|
|
|
|
|
var headers = new Metadata(); |
|
|
|
statsWatcher.OnRpcComplete(rpcId, response.Hostname); |
|
|
|
if (metadata.ContainsKey(rpcType)) |
|
|
|
if (options.PrintResponse) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
Console.WriteLine($"Got response {response}"); |
|
|
|
headers = metadata[rpcType]; |
|
|
|
|
|
|
|
if (headers.Count > 0) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var printableHeaders = "[" + string.Join(", ", headers) + "]"; |
|
|
|
|
|
|
|
Console.WriteLine($"Will send metadata {printableHeaders}"); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Console.WriteLine($"RPC {rpcId} succeeded "); |
|
|
|
|
|
|
|
|
|
|
|
if (rpcType == RpcType.UnaryCall) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var call = client.UnaryCallAsync(new SimpleRequest(), |
|
|
|
|
|
|
|
new CallOptions(headers: headers, cancellationToken: cancellationToken, deadline: DateTime.UtcNow.AddSeconds(options.RpcTimeoutSec))); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var response = await call; |
|
|
|
|
|
|
|
var hostname = (await call.ResponseHeadersAsync).GetValue("hostname") ?? response.Hostname; |
|
|
|
|
|
|
|
statsWatcher.OnRpcComplete(rpcId, rpcType, hostname); |
|
|
|
|
|
|
|
if (options.PrintResponse) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Console.WriteLine($"Got response {response}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (rpcType == RpcType.EmptyCall) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var call = client.EmptyCallAsync(new Empty(), |
|
|
|
|
|
|
|
new CallOptions(headers: headers, cancellationToken: cancellationToken, deadline: DateTime.UtcNow.AddSeconds(options.RpcTimeoutSec))); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var response = await call; |
|
|
|
|
|
|
|
var hostname = (await call.ResponseHeadersAsync).GetValue("hostname"); |
|
|
|
|
|
|
|
statsWatcher.OnRpcComplete(rpcId, rpcType, hostname); |
|
|
|
|
|
|
|
if (options.PrintResponse) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Console.WriteLine($"Got response {response}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
throw new InvalidOperationException($"Unsupported RPC type ${rpcType}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Console.WriteLine($"RPC {rpcId} succeeded"); |
|
|
|
} |
|
|
|
} |
|
|
|
catch (RpcException ex) |
|
|
|
catch (RpcException ex) |
|
|
|
{ |
|
|
|
{ |
|
|
|
statsWatcher.OnRpcComplete(rpcId, null); |
|
|
|
statsWatcher.OnRpcComplete(rpcId, rpcType, null); |
|
|
|
Console.WriteLine($"RPC {rpcId} failed: {ex}"); |
|
|
|
Console.WriteLine($"RPC {rpcId} failed: {ex}"); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -186,6 +244,66 @@ namespace Grpc.IntegrationTesting |
|
|
|
tasks.Remove(task); |
|
|
|
tasks.Remove(task); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static List<RpcType> ParseRpcArgument(string rpcArg) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var result = new List<RpcType>(); |
|
|
|
|
|
|
|
foreach (var part in rpcArg.Split(',')) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.Add(ParseRpc(part)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static RpcType ParseRpc(string rpc) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
switch (rpc) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
case "UnaryCall": |
|
|
|
|
|
|
|
return RpcType.UnaryCall; |
|
|
|
|
|
|
|
case "EmptyCall": |
|
|
|
|
|
|
|
return RpcType.EmptyCall; |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
throw new ArgumentException($"Unknown RPC: \"{rpc}\""); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static Dictionary<RpcType, Metadata> ParseMetadataArgument(string metadataArg) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var rpcMetadata = new Dictionary<RpcType, Metadata>(); |
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(metadataArg)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return rpcMetadata; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var metadata in metadataArg.Split(',')) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var parts = metadata.Split(':'); |
|
|
|
|
|
|
|
if (parts.Length != 3) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
throw new ArgumentException($"Invalid metadata: \"{metadata}\""); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
var rpc = ParseRpc(parts[0]); |
|
|
|
|
|
|
|
var key = parts[1]; |
|
|
|
|
|
|
|
var value = parts[2]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var md = new Metadata { {key, value} }; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (rpcMetadata.ContainsKey(rpc)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var existingMetadata = rpcMetadata[rpc]; |
|
|
|
|
|
|
|
foreach (var entry in md) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
existingMetadata.Add(entry); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
rpcMetadata.Add(rpc, md); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return rpcMetadata; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
internal class StatsWatcher |
|
|
|
internal class StatsWatcher |
|
|
@ -198,6 +316,7 @@ namespace Grpc.IntegrationTesting |
|
|
|
private int rpcsCompleted; |
|
|
|
private int rpcsCompleted; |
|
|
|
private int rpcsNoHostname; |
|
|
|
private int rpcsNoHostname; |
|
|
|
private Dictionary<string, int> rpcsByHostname; |
|
|
|
private Dictionary<string, int> rpcsByHostname; |
|
|
|
|
|
|
|
private Dictionary<string, Dictionary<string, int>> rpcsByMethod; |
|
|
|
|
|
|
|
|
|
|
|
public AtomicCounter RpcIdGenerator => rpcIdGenerator; |
|
|
|
public AtomicCounter RpcIdGenerator => rpcIdGenerator; |
|
|
|
|
|
|
|
|
|
|
@ -206,7 +325,7 @@ namespace Grpc.IntegrationTesting |
|
|
|
Reset(); |
|
|
|
Reset(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void OnRpcComplete(long rpcId, string responseHostname) |
|
|
|
public void OnRpcComplete(long rpcId, XdsInteropClient.RpcType rpcType, string responseHostname) |
|
|
|
{ |
|
|
|
{ |
|
|
|
lock (myLock) |
|
|
|
lock (myLock) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -221,11 +340,24 @@ namespace Grpc.IntegrationTesting |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
else |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
// update rpcsByHostname |
|
|
|
if (!rpcsByHostname.ContainsKey(responseHostname)) |
|
|
|
if (!rpcsByHostname.ContainsKey(responseHostname)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
rpcsByHostname[responseHostname] = 0; |
|
|
|
rpcsByHostname[responseHostname] = 0; |
|
|
|
} |
|
|
|
} |
|
|
|
rpcsByHostname[responseHostname] += 1; |
|
|
|
rpcsByHostname[responseHostname] += 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// update rpcsByMethod |
|
|
|
|
|
|
|
var method = rpcType.ToString(); |
|
|
|
|
|
|
|
if (!rpcsByMethod.ContainsKey(method)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
rpcsByMethod[method] = new Dictionary<string, int>(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!rpcsByMethod[method].ContainsKey(responseHostname)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
rpcsByMethod[method][responseHostname] = 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
rpcsByMethod[method][responseHostname] += 1; |
|
|
|
} |
|
|
|
} |
|
|
|
rpcsCompleted += 1; |
|
|
|
rpcsCompleted += 1; |
|
|
|
|
|
|
|
|
|
|
@ -245,6 +377,7 @@ namespace Grpc.IntegrationTesting |
|
|
|
rpcsCompleted = 0; |
|
|
|
rpcsCompleted = 0; |
|
|
|
rpcsNoHostname = 0; |
|
|
|
rpcsNoHostname = 0; |
|
|
|
rpcsByHostname = new Dictionary<string, int>(); |
|
|
|
rpcsByHostname = new Dictionary<string, int>(); |
|
|
|
|
|
|
|
rpcsByMethod = new Dictionary<string, Dictionary<string, int>>(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -269,6 +402,14 @@ namespace Grpc.IntegrationTesting |
|
|
|
// we collected enough RPCs, or timed out waiting |
|
|
|
// we collected enough RPCs, or timed out waiting |
|
|
|
var response = new LoadBalancerStatsResponse { NumFailures = rpcsNoHostname }; |
|
|
|
var response = new LoadBalancerStatsResponse { NumFailures = rpcsNoHostname }; |
|
|
|
response.RpcsByPeer.Add(rpcsByHostname); |
|
|
|
response.RpcsByPeer.Add(rpcsByHostname); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
response.RpcsByMethod.Clear(); |
|
|
|
|
|
|
|
foreach (var methodEntry in rpcsByMethod) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var rpcsByPeer = new LoadBalancerStatsResponse.Types.RpcsByPeer(); |
|
|
|
|
|
|
|
rpcsByPeer.RpcsByPeer_.Add(methodEntry.Value); |
|
|
|
|
|
|
|
response.RpcsByMethod[methodEntry.Key] = rpcsByPeer; |
|
|
|
|
|
|
|
} |
|
|
|
Reset(); |
|
|
|
Reset(); |
|
|
|
return response; |
|
|
|
return response; |
|
|
|
} |
|
|
|
} |
|
|
|