agones.dev/agones@v1.53.0/sdks/csharp/sdk/AgonesSDK.cs (about) 1 // Copyright 2020 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 using Agones.Dev.Sdk; 15 using Microsoft.Extensions.Logging; 16 using System; 17 using System.Threading; 18 using System.Threading.Tasks; 19 using Grpc.Core; 20 using Grpc.Net.Client; 21 22 [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Agones.Test")] 23 namespace Agones 24 { 25 public sealed class AgonesSDK : IAgonesSDK 26 { 27 public string Host { get; } = Environment.GetEnvironmentVariable("AGONES_SDK_GRPC_HOST") ?? "localhost"; 28 public int Port { get; } = Convert.ToInt32(Environment.GetEnvironmentVariable("AGONES_SDK_GRPC_PORT") ?? "9357"); 29 30 /// <summary> 31 /// The timeout for gRPC calls. 32 /// </summary> 33 public double RequestTimeoutSec { get; set; } 34 35 internal SDK.SDKClient client; 36 internal readonly Alpha alpha; 37 internal readonly Beta beta; 38 internal readonly GrpcChannel channel; 39 internal AsyncClientStreamingCall<Empty,Empty> healthStream; 40 internal readonly CancellationTokenSource cts; 41 internal readonly bool ownsCts; 42 internal CancellationToken ctoken; 43 internal volatile bool isWatchingGameServer; 44 45 /// <summary> 46 /// Fired every time the GameServer k8s data is updated. 47 /// A more efficient way to emulate WatchGameServer behavior 48 /// without starting a new task for every subscription request. 49 /// </summary> 50 private event Action<GameServer> GameServerUpdated; 51 internal Delegate[] GameServerUpdatedCallbacks => GameServerUpdated?.GetInvocationList(); 52 private readonly ILogger _logger; 53 private readonly SemaphoreSlim _healthStreamSemaphore = new SemaphoreSlim(1, 1); 54 private readonly object _gameServerWatchSyncRoot = new object(); 55 56 private bool _disposed; 57 58 public AgonesSDK( 59 double requestTimeoutSec = 15, 60 SDK.SDKClient sdkClient = null, 61 CancellationTokenSource cancellationTokenSource = null, 62 ILogger logger = null) 63 { 64 _logger = logger; 65 RequestTimeoutSec = requestTimeoutSec; 66 67 if (cancellationTokenSource == null) 68 { 69 cts = new CancellationTokenSource(); 70 ownsCts = true; 71 } 72 else 73 { 74 cts = cancellationTokenSource; 75 ownsCts = false; 76 } 77 78 ctoken = cts.Token; 79 channel = GrpcChannel.ForAddress( 80 $"http://{Host}:{Port}" 81 ); 82 83 client = sdkClient ?? new SDK.SDKClient(channel); 84 alpha = new Alpha(channel, requestTimeoutSec, cancellationTokenSource, logger); 85 beta = new Beta(channel, requestTimeoutSec, cancellationTokenSource, logger); 86 } 87 88 /// <summary> 89 /// Alpha returns the Alpha SDK 90 /// </summary> 91 /// <returns>Agones alpha SDK</returns> 92 public IAgonesAlphaSDK Alpha() 93 { 94 return alpha; 95 } 96 97 /// <summary> 98 /// Beta returns the AlphBeta SDK 99 /// </summary> 100 /// <returns>Agones beta SDK</returns> 101 public IAgonesBetaSDK Beta() 102 { 103 return beta; 104 } 105 106 /// <summary> 107 /// Tells Agones that the Game Server is ready to take player connections 108 /// </summary> 109 /// <returns>gRPC Status of the request</returns> 110 public async Task<Status> ReadyAsync() 111 { 112 try 113 { 114 await client.ReadyAsync(new Empty(), deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 115 return new Status(StatusCode.OK, "Ready request successful."); 116 } 117 catch (RpcException ex) 118 { 119 LogError("Unable to mark GameServer to 'Ready' state.", ex); 120 return ex.Status; 121 } 122 } 123 124 /// <summary> 125 /// Marks the game server as Allocated. 126 /// </summary> 127 /// <returns>gRPC Status of the request</returns> 128 public async Task<Status> AllocateAsync() 129 { 130 try 131 { 132 await client.AllocateAsync(new Empty(), 133 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), 134 cancellationToken: ctoken); 135 return new Status(StatusCode.OK, "Allocate request successful."); 136 } 137 catch (RpcException ex) 138 { 139 LogError("Unable to mark the GameServer to 'Allocated' state.", ex); 140 return ex.Status; 141 } 142 } 143 144 /// <summary> 145 /// Reserve(seconds) will move the GameServer into the Reserved state for the specified number of seconds (0 is forever), 146 /// and then it will be moved back to Ready state. While in Reserved state, 147 /// the GameServer will not be deleted on scale down or Fleet update, and also it could not be Allocated using GameServerAllocation. 148 /// </summary> 149 /// <param name="seconds">Amount of seconds to reserve.</param> 150 /// <returns>gRPC Status of the request</returns> 151 public async Task<Status> ReserveAsync(long seconds) 152 { 153 try 154 { 155 await client.ReserveAsync(new Duration { Seconds = seconds}, 156 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), 157 cancellationToken: ctoken); 158 return new Status(StatusCode.OK, $"Reserve({seconds}) request successful."); 159 } 160 catch (RpcException ex) 161 { 162 LogError("Unable to mark the GameServer to 'Reserved' state.", ex); 163 return ex.Status; 164 } 165 } 166 167 /// <summary> 168 /// This returns most of the backing GameServer configuration and Status. 169 /// This can be useful for instances where you may want to know Health check configuration, or the IP and Port the GameServer is currently allocated to. 170 /// </summary> 171 /// <returns>A GameServer object containing this GameServer's configuration data</returns> 172 public async Task<GameServer> GetGameServerAsync() 173 { 174 try 175 { 176 return await client.GetGameServerAsync(new Empty(), 177 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), 178 cancellationToken: ctoken); 179 } 180 catch (RpcException ex) 181 { 182 LogError("Unable to get GameServer configuration and status.", ex); 183 throw; 184 } 185 } 186 187 /// <summary> 188 /// Starts watching the GameServer updates in the background in it's own task. 189 /// On update, it fires the GameServerUpdate event. 190 /// </summary> 191 private async Task BeginInternalWatchAsync() 192 { 193 // Begin WatchGameServer in the background for the provided callback(s). 194 while (!ctoken.IsCancellationRequested) 195 { 196 try 197 { 198 using (var watchStreamingCall = client.WatchGameServer(new Empty(), cancellationToken: ctoken)) 199 { 200 var reader = watchStreamingCall.ResponseStream; 201 while (await reader.MoveNext(ctoken)) 202 { 203 try 204 { 205 GameServerUpdated?.Invoke(reader.Current); 206 } 207 catch (Exception ex) 208 { 209 // Swallow any exception thrown here. We don't want a callback's exception to cause 210 // our watch to be torn down. 211 LogWarning($"A {nameof(WatchGameServer)} callback threw an exception", ex); 212 } 213 } 214 } 215 } 216 catch (OperationCanceledException) when (ctoken.IsCancellationRequested) 217 { 218 return; 219 } 220 catch (RpcException) when (ctoken.IsCancellationRequested) 221 { 222 return; 223 } 224 catch (RpcException ex) 225 { 226 LogError("An error occurred while watching GameServer events, will retry.", ex); 227 } 228 } 229 } 230 231 /// <summary> 232 /// This executes the passed in callback with the current GameServer details whenever the underlying GameServer configuration is updated. 233 /// This can be useful to track GameServer > Status > State changes, metadata changes, such as labels and annotations, and more. 234 /// </summary> 235 /// <param name="callback">The action to be called when the underlying GameServer metadata changes.</param> 236 public void WatchGameServer(Action<GameServer> callback) 237 { 238 GameServerUpdated += callback; 239 240 lock (_gameServerWatchSyncRoot) 241 { 242 if (isWatchingGameServer) 243 { 244 return; 245 } 246 247 isWatchingGameServer = true; 248 } 249 250 // Kick off the watch in a task so the caller doesn't need to handle exceptions that could potentially be 251 // thrown before reaching the first yielding async point. 252 Task.Run(async () => await BeginInternalWatchAsync(), ctoken); 253 } 254 255 /// <summary> 256 /// Cancels all running tasks & tells Agones to shut down the currently running game server. 257 /// </summary> 258 /// <returns>gRPC Status of the request</returns> 259 public async Task<Status> ShutDownAsync() 260 { 261 try 262 { 263 await client.ShutdownAsync(new Empty(), deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec)); 264 return new Status(StatusCode.OK, "Shutdown request successful."); 265 } 266 catch (RpcException ex) 267 { 268 LogError("Unable to mark the GameServer to 'Shutdown' state.", ex); 269 return ex.Status; 270 } 271 } 272 273 /// <summary> 274 /// Set a Label value on the backing GameServer record that is stored in Kubernetes, with the prefix 'agones.dev/sdk-'. 275 /// </summary> 276 /// <param name="key">Label key</param> 277 /// <param name="value">Label value</param> 278 /// <returns>gRPC Status of the request</returns> 279 public async Task<Status> SetLabelAsync(string key, string value) 280 { 281 try 282 { 283 await client.SetLabelAsync(new KeyValue() 284 { 285 Key = key, 286 Value = value 287 }, deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 288 return new Status(StatusCode.OK, $"SetLabel {key}:{value} request successful."); 289 } 290 catch(RpcException ex) 291 { 292 LogError($"Unable to set the GameServer label '{key}' to '{value}'.", ex); 293 return ex.Status; 294 } 295 } 296 297 /// <summary> 298 /// Set a Annotation value on the backing Gameserver record that is stored in Kubernetes, with the prefix 'agones.dev/sdk-'. 299 /// </summary> 300 /// <param name="key">Annotation key</param> 301 /// <param name="value">Annotation value</param> 302 /// <returns>gRPC Status of the request</returns> 303 public async Task<Status> SetAnnotationAsync(string key, string value) 304 { 305 try 306 { 307 await client.SetAnnotationAsync(new KeyValue() 308 { 309 Key = key, 310 Value = value 311 }, deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 312 return new Status(StatusCode.OK, $"SetAnnotation {key}:{value} request successful."); 313 } 314 catch (RpcException ex) 315 { 316 LogError($"Unable to set the GameServer annotation '{key}' to '{value}'.", ex); 317 return ex.Status; 318 } 319 } 320 321 /// <summary> 322 /// Sends a single ping to designate that the Game Server is alive and healthy. 323 /// </summary> 324 /// <returns>gRPC Status of the request</returns> 325 public async Task<Status> HealthAsync() 326 { 327 try 328 { 329 await _healthStreamSemaphore.WaitAsync(ctoken); 330 } 331 catch (OperationCanceledException) 332 { 333 return Status.DefaultCancelled; 334 } 335 336 try 337 { 338 if (healthStream == null) 339 { 340 // Create a new stream if it's the first time we're being called or if the previous stream threw 341 // an exception. 342 healthStream = client.Health(); 343 } 344 345 var writer = healthStream.RequestStream; 346 await writer.WriteAsync(new Empty()); 347 return new Status(StatusCode.OK, "Health ping successful."); 348 } 349 catch (RpcException ex) 350 { 351 LogError("Unable to invoke the GameServer health check.", ex); 352 353 if (healthStream != null) 354 { 355 try 356 { 357 // Best effort to clean up. 358 healthStream.Dispose(); 359 } 360 catch (Exception innerEx) 361 { 362 LogWarning($"Failed to dispose existing {nameof(client.Health)} client stream", innerEx); 363 } 364 } 365 366 // Null out the stream so the subsequent call causes it to be recreated. 367 healthStream = null; 368 return ex.Status; 369 } 370 finally 371 { 372 _healthStreamSemaphore.Release(); 373 } 374 } 375 376 public void Dispose() 377 { 378 if (_disposed) 379 { 380 return; 381 } 382 383 cts.Cancel(); 384 385 if (ownsCts) 386 { 387 cts.Dispose(); 388 } 389 390 channel.Dispose(); 391 392 // Since we don't provide any facility to unregister a WatchGameServer callback, set the event to null to 393 // clear its underlying invocation list, so we don't keep holding references to objects that would prevent 394 // them to be GC'd in case we don't go out of scope. 395 GameServerUpdated = null; 396 397 _disposed = true; 398 GC.SuppressFinalize(this); 399 } 400 401 private void LogDebug(string message, Exception ex = null) 402 { 403 _logger?.LogDebug(ex, message); 404 } 405 406 private void LogInformation(string message, Exception ex = null) 407 { 408 _logger?.LogInformation(ex, message); 409 } 410 411 private void LogWarning(string message, Exception ex = null) 412 { 413 _logger?.LogWarning(ex, message); 414 } 415 416 private void LogError(string message, Exception ex = null) 417 { 418 _logger?.LogError(ex, message); 419 } 420 } 421 }