1 #region Copyright notice and license 2 // Copyright 2015 gRPC authors. 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 #endregion 16 17 using System; 18 using System.Collections.Generic; 19 using System.Diagnostics; 20 using System.Linq; 21 using System.Text; 22 using System.Threading; 23 using System.Threading.Tasks; 24 25 using Grpc.Core; 26 using Grpc.Health.V1; 27 using NUnit.Framework; 28 29 namespace Grpc.HealthCheck.Tests 30 { 31 /// <summary> 32 /// Tests for HealthCheckServiceImpl 33 /// </summary> 34 public class HealthServiceImplTest 35 { 36 [Test] SetStatus()37 public void SetStatus() 38 { 39 var impl = new HealthServiceImpl(); 40 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Serving); 41 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Serving, GetStatusHelper(impl, "")); 42 43 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.NotServing); 44 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.NotServing, GetStatusHelper(impl, "")); 45 46 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Unknown); 47 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Unknown, GetStatusHelper(impl, "")); 48 49 impl.SetStatus("grpc.test.TestService", HealthCheckResponse.Types.ServingStatus.Serving); 50 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Serving, GetStatusHelper(impl, "grpc.test.TestService")); 51 } 52 53 [Test] ClearStatus()54 public void ClearStatus() 55 { 56 var impl = new HealthServiceImpl(); 57 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Serving); 58 impl.SetStatus("grpc.test.TestService", HealthCheckResponse.Types.ServingStatus.Unknown); 59 60 impl.ClearStatus(""); 61 62 var ex = Assert.Throws<RpcException>(() => GetStatusHelper(impl, "")); 63 Assert.AreEqual(StatusCode.NotFound, ex.Status.StatusCode); 64 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Unknown, GetStatusHelper(impl, "grpc.test.TestService")); 65 } 66 67 [Test] ClearAll()68 public void ClearAll() 69 { 70 var impl = new HealthServiceImpl(); 71 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Serving); 72 impl.SetStatus("grpc.test.TestService", HealthCheckResponse.Types.ServingStatus.Unknown); 73 74 impl.ClearAll(); 75 Assert.Throws(typeof(RpcException), () => GetStatusHelper(impl, "")); 76 Assert.Throws(typeof(RpcException), () => GetStatusHelper(impl, "grpc.test.TestService")); 77 } 78 79 [Test] NullsRejected()80 public void NullsRejected() 81 { 82 var impl = new HealthServiceImpl(); 83 Assert.Throws(typeof(ArgumentNullException), () => impl.SetStatus(null, HealthCheckResponse.Types.ServingStatus.Serving)); 84 85 Assert.Throws(typeof(ArgumentNullException), () => impl.ClearStatus(null)); 86 } 87 88 #if GRPC_SUPPORT_WATCH 89 [Test] Watch()90 public async Task Watch() 91 { 92 var cts = new CancellationTokenSource(); 93 var context = new TestServerCallContext(cts.Token); 94 var writer = new TestResponseStreamWriter(); 95 96 var impl = new HealthServiceImpl(); 97 var callTask = impl.Watch(new HealthCheckRequest { Service = "" }, writer, context); 98 99 // Calling Watch on a service that doesn't have a value set will initially return ServiceUnknown 100 var nextWriteTask = writer.WrittenMessagesReader.ReadAsync(); 101 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask).Status); 102 103 nextWriteTask = writer.WrittenMessagesReader.ReadAsync(); 104 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Serving); 105 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Serving, (await nextWriteTask).Status); 106 107 nextWriteTask = writer.WrittenMessagesReader.ReadAsync(); 108 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.NotServing); 109 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.NotServing, (await nextWriteTask).Status); 110 111 nextWriteTask = writer.WrittenMessagesReader.ReadAsync(); 112 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Unknown); 113 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Unknown, (await nextWriteTask).Status); 114 115 // Setting status for a different service name will not update Watch results 116 nextWriteTask = writer.WrittenMessagesReader.ReadAsync(); 117 impl.SetStatus("grpc.test.TestService", HealthCheckResponse.Types.ServingStatus.Serving); 118 Assert.IsFalse(nextWriteTask.IsCompleted); 119 120 impl.ClearStatus(""); 121 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask).Status); 122 123 Assert.IsFalse(callTask.IsCompleted); 124 cts.Cancel(); 125 await callTask; 126 } 127 128 [Test] Watch_MultipleWatchesForSameService()129 public async Task Watch_MultipleWatchesForSameService() 130 { 131 var cts = new CancellationTokenSource(); 132 var context = new TestServerCallContext(cts.Token); 133 var writer1 = new TestResponseStreamWriter(); 134 var writer2 = new TestResponseStreamWriter(); 135 136 var impl = new HealthServiceImpl(); 137 var callTask1 = impl.Watch(new HealthCheckRequest { Service = "" }, writer1, context); 138 var callTask2 = impl.Watch(new HealthCheckRequest { Service = "" }, writer2, context); 139 140 // Calling Watch on a service that doesn't have a value set will initially return ServiceUnknown 141 var nextWriteTask1 = writer1.WrittenMessagesReader.ReadAsync(); 142 var nextWriteTask2 = writer2.WrittenMessagesReader.ReadAsync(); 143 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask1).Status); 144 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask2).Status); 145 146 nextWriteTask1 = writer1.WrittenMessagesReader.ReadAsync(); 147 nextWriteTask2 = writer2.WrittenMessagesReader.ReadAsync(); 148 impl.SetStatus("", HealthCheckResponse.Types.ServingStatus.Serving); 149 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Serving, (await nextWriteTask1).Status); 150 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Serving, (await nextWriteTask2).Status); 151 152 nextWriteTask1 = writer1.WrittenMessagesReader.ReadAsync(); 153 nextWriteTask2 = writer2.WrittenMessagesReader.ReadAsync(); 154 impl.ClearStatus(""); 155 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask1).Status); 156 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask2).Status); 157 158 cts.Cancel(); 159 await callTask1; 160 await callTask2; 161 } 162 163 [Test] Watch_MultipleWatchesForDifferentServices()164 public async Task Watch_MultipleWatchesForDifferentServices() 165 { 166 var cts = new CancellationTokenSource(); 167 var context = new TestServerCallContext(cts.Token); 168 var writer1 = new TestResponseStreamWriter(); 169 var writer2 = new TestResponseStreamWriter(); 170 171 var impl = new HealthServiceImpl(); 172 var callTask1 = impl.Watch(new HealthCheckRequest { Service = "One" }, writer1, context); 173 var callTask2 = impl.Watch(new HealthCheckRequest { Service = "Two" }, writer2, context); 174 175 // Calling Watch on a service that doesn't have a value set will initially return ServiceUnknown 176 var nextWriteTask1 = writer1.WrittenMessagesReader.ReadAsync(); 177 var nextWriteTask2 = writer2.WrittenMessagesReader.ReadAsync(); 178 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask1).Status); 179 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask2).Status); 180 181 nextWriteTask1 = writer1.WrittenMessagesReader.ReadAsync(); 182 nextWriteTask2 = writer2.WrittenMessagesReader.ReadAsync(); 183 impl.SetStatus("One", HealthCheckResponse.Types.ServingStatus.Serving); 184 impl.SetStatus("Two", HealthCheckResponse.Types.ServingStatus.NotServing); 185 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.Serving, (await nextWriteTask1).Status); 186 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.NotServing, (await nextWriteTask2).Status); 187 188 nextWriteTask1 = writer1.WrittenMessagesReader.ReadAsync(); 189 nextWriteTask2 = writer2.WrittenMessagesReader.ReadAsync(); 190 impl.ClearAll(); 191 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask1).Status); 192 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, (await nextWriteTask2).Status); 193 194 cts.Cancel(); 195 await callTask1; 196 await callTask2; 197 } 198 199 [Test] Watch_ExceedMaximumCapacitySize_DiscardOldValues()200 public async Task Watch_ExceedMaximumCapacitySize_DiscardOldValues() 201 { 202 var cts = new CancellationTokenSource(); 203 var context = new TestServerCallContext(cts.Token); 204 var writer = new TestResponseStreamWriter(started: false); 205 206 var impl = new HealthServiceImpl(); 207 var callTask = impl.Watch(new HealthCheckRequest { Service = "" }, writer, context); 208 209 // Write new statuses. Only last statuses will be returned when we read them from watch writer 210 for (var i = 0; i < HealthServiceImpl.MaxStatusBufferSize * 2; i++) 211 { 212 // These statuses aren't "valid" but it is useful for testing to have an incrementing number 213 impl.SetStatus("", (HealthCheckResponse.Types.ServingStatus)i + 10); 214 } 215 216 // Start reading responses now that statuses have been queued up 217 // This is to keep the test non-flakey 218 writer.Start(); 219 220 // Read messages in a background task 221 var statuses = new List<HealthCheckResponse.Types.ServingStatus>(); 222 var readStatusesTask = Task.Run(async () => { 223 while (await writer.WrittenMessagesReader.WaitToReadAsync()) 224 { 225 if (writer.WrittenMessagesReader.TryRead(out var response)) 226 { 227 statuses.Add(response.Status); 228 } 229 } 230 }); 231 232 // Tell server we're done watching and it can write what it has left and then exit 233 cts.Cancel(); 234 await callTask; 235 236 // Ensure we've read all the queued statuses 237 writer.Complete(); 238 await readStatusesTask; 239 240 // Collection will contain initial written message (ServiceUnknown) plus 5 queued messages 241 Assert.AreEqual(HealthServiceImpl.MaxStatusBufferSize + 1, statuses.Count); 242 243 // Initial written message 244 Assert.AreEqual(HealthCheckResponse.Types.ServingStatus.ServiceUnknown, statuses[0]); 245 246 // Last 5 queued messages 247 Assert.AreEqual((HealthCheckResponse.Types.ServingStatus)15, statuses[statuses.Count - 5]); 248 Assert.AreEqual((HealthCheckResponse.Types.ServingStatus)16, statuses[statuses.Count - 4]); 249 Assert.AreEqual((HealthCheckResponse.Types.ServingStatus)17, statuses[statuses.Count - 3]); 250 Assert.AreEqual((HealthCheckResponse.Types.ServingStatus)18, statuses[statuses.Count - 2]); 251 Assert.AreEqual((HealthCheckResponse.Types.ServingStatus)19, statuses[statuses.Count - 1]); 252 } 253 #endif 254 GetStatusHelper(HealthServiceImpl impl, string service)255 private static HealthCheckResponse.Types.ServingStatus GetStatusHelper(HealthServiceImpl impl, string service) 256 { 257 return impl.Check(new HealthCheckRequest { Service = service }, null).Result.Status; 258 } 259 } 260 } 261